001package org.apache.maven.scm; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import java.io.Serializable; 023import java.text.ParseException; 024import java.text.SimpleDateFormat; 025import java.util.ArrayList; 026import java.util.Collections; 027import java.util.Date; 028import java.util.LinkedHashSet; 029import java.util.List; 030import java.util.Set; 031 032import org.apache.maven.scm.provider.ScmProviderRepository; 033import org.apache.maven.scm.util.FilenameUtils; 034import org.apache.maven.scm.util.ThreadSafeDateFormat; 035import org.codehaus.plexus.util.StringUtils; 036 037/** 038 * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a> 039 * 040 */ 041public class ChangeSet 042 implements Serializable 043{ 044 /** 045 * 046 */ 047 private static final long serialVersionUID = 7097705862222539801L; 048 049 /** 050 * Escaped <code><</code> entity 051 */ 052 public static final String LESS_THAN_ENTITY = "<"; 053 054 /** 055 * Escaped <code>></code> entity 056 */ 057 public static final String GREATER_THAN_ENTITY = ">"; 058 059 /** 060 * Escaped <code>&</code> entity 061 */ 062 public static final String AMPERSAND_ENTITY = "&"; 063 064 /** 065 * Escaped <code>'</code> entity 066 */ 067 public static final String APOSTROPHE_ENTITY = "'"; 068 069 /** 070 * Escaped <code>"</code> entity 071 */ 072 public static final String QUOTE_ENTITY = """; 073 074 private static final String DATE_PATTERN = "yyyy-MM-dd"; 075 076 /** 077 * Formatter used by the getDateFormatted method. 078 */ 079 private static final ThreadSafeDateFormat DATE_FORMAT = new ThreadSafeDateFormat( DATE_PATTERN ); 080 081 private static final String TIME_PATTERN = "HH:mm:ss"; 082 083 /** 084 * Formatter used by the getTimeFormatted method. 085 */ 086 private static final ThreadSafeDateFormat TIME_FORMAT = new ThreadSafeDateFormat( TIME_PATTERN ); 087 088 /** 089 * Formatter used to parse date/timestamp. 090 */ 091 private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_1 = new ThreadSafeDateFormat( "yyyy/MM/dd HH:mm:ss" ); 092 093 private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_2 = new ThreadSafeDateFormat( "yyyy-MM-dd HH:mm:ss" ); 094 095 private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_3 = new ThreadSafeDateFormat( "yyyy/MM/dd HH:mm:ss z" ); 096 097 private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_4 = new ThreadSafeDateFormat( "yyyy-MM-dd HH:mm:ss z" ); 098 099 /** 100 * Date the changes were committed 101 */ 102 private Date date; 103 104 /** 105 * User who made changes 106 */ 107 private String author; 108 109 /** 110 * comment provided at commit time 111 */ 112 private String comment = ""; 113 114 /** 115 * List of ChangeFile 116 */ 117 private List<ChangeFile> files; 118 119 /** 120 * The SCM revision id for this changeset. 121 * @since 1.3 122 */ 123 private String revision; 124 125 /** 126 * Revision from which this one originates 127 * @since 1.7 128 */ 129 private String parentRevision; 130 131 /** 132 * Revisions that were merged into this one 133 * @since 1.7 134 */ 135 private Set<String> mergedRevisions; 136 137 /** 138 * @param strDate Date the changes were committed 139 * @param userDatePattern pattern of date 140 * @param comment comment provided at commit time 141 * @param author User who made changes 142 * @param files The ChangeFile list 143 */ 144 public ChangeSet( String strDate, String userDatePattern, String comment, String author, 145 List<ChangeFile> files ) 146 { 147 this( null, comment, author, files ); 148 149 setDate( strDate, userDatePattern ); 150 } 151 152 /** 153 * @param date Date the changes were committed 154 * @param comment comment provided at commit time 155 * @param author User who made changes 156 * @param files The ChangeFile list 157 */ 158 public ChangeSet( Date date, String comment, String author, List<ChangeFile> files ) 159 { 160 setDate( date ); 161 162 setAuthor( author ); 163 164 setComment( comment ); 165 166 this.files = files; 167 } 168 169 /** 170 * Constructor used when attributes aren't available until later 171 */ 172 public ChangeSet() 173 { 174 // no op 175 } 176 177 /** 178 * Getter for ChangeFile list. 179 * 180 * @return List of ChangeFile. 181 */ 182 public List<ChangeFile> getFiles() 183 { 184 if ( files == null ) 185 { 186 return new ArrayList<ChangeFile>(); 187 } 188 return files; 189 } 190 191 /** 192 * Setter for ChangeFile list. 193 * 194 * @param files List of ChangeFiles. 195 */ 196 public void setFiles( List<ChangeFile> files ) 197 { 198 this.files = files; 199 } 200 201 public void addFile( ChangeFile file ) 202 { 203 if ( files == null ) 204 { 205 files = new ArrayList<ChangeFile>(); 206 } 207 208 files.add( file ); 209 } 210 211 /** 212 * @deprecated Use method {@link #containsFilename(String)} 213 * @param filename 214 * @param repository NOT USED 215 * @return 216 */ 217 public boolean containsFilename( String filename, ScmProviderRepository repository ) 218 { 219 return containsFilename( filename ); 220 } 221 222 public boolean containsFilename( String filename ) 223 { 224 if ( files != null ) 225 { 226 for ( ChangeFile file : files ) 227 { 228 String f1 = FilenameUtils.normalizeFilename( file.getName() ); 229 String f2 = FilenameUtils.normalizeFilename( filename ); 230 if ( f1.indexOf( f2 ) >= 0 ) 231 { 232 return true; 233 } 234 } 235 } 236 237 return false; 238 } 239 240 /** 241 * Getter for property author. 242 * 243 * @return Value of property author. 244 */ 245 public String getAuthor() 246 { 247 return author; 248 } 249 250 /** 251 * Setter for property author. 252 * 253 * @param author New value of property author. 254 */ 255 public void setAuthor( String author ) 256 { 257 this.author = author; 258 } 259 260 /** 261 * Getter for property comment. 262 * 263 * @return Value of property comment. 264 */ 265 public String getComment() 266 { 267 return comment; 268 } 269 270 /** 271 * Setter for property comment. 272 * 273 * @param comment New value of property comment. 274 */ 275 public void setComment( String comment ) 276 { 277 this.comment = comment; 278 } 279 280 /** 281 * Getter for property date. 282 * 283 * @return Value of property date. 284 */ 285 public Date getDate() 286 { 287 if ( date != null ) 288 { 289 return (Date) date.clone(); 290 } 291 292 return null; 293 } 294 295 /** 296 * Setter for property date. 297 * 298 * @param date New value of property date. 299 */ 300 public void setDate( Date date ) 301 { 302 if ( date != null ) 303 { 304 this.date = new Date( date.getTime() ); 305 } 306 } 307 308 /** 309 * Setter for property date that takes a string and parses it 310 * 311 * @param date - a string in yyyy/MM/dd HH:mm:ss format 312 */ 313 public void setDate( String date ) 314 { 315 setDate( date, null ); 316 } 317 318 /** 319 * Setter for property date that takes a string and parses it 320 * 321 * @param date - a string in yyyy/MM/dd HH:mm:ss format 322 * @param userDatePattern - pattern of date 323 */ 324 public void setDate( String date, String userDatePattern ) 325 { 326 try 327 { 328 if ( !StringUtils.isEmpty( userDatePattern ) ) 329 { 330 SimpleDateFormat format = new SimpleDateFormat( userDatePattern ); 331 332 this.date = format.parse( date ); 333 } 334 else 335 { 336 this.date = TIMESTAMP_FORMAT_3.parse( date ); 337 } 338 } 339 catch ( ParseException e ) 340 { 341 if ( !StringUtils.isEmpty( userDatePattern ) ) 342 { 343 try 344 { 345 this.date = TIMESTAMP_FORMAT_3.parse( date ); 346 } 347 catch ( ParseException pe ) 348 { 349 try 350 { 351 this.date = TIMESTAMP_FORMAT_4.parse( date ); 352 } 353 catch ( ParseException pe1 ) 354 { 355 try 356 { 357 this.date = TIMESTAMP_FORMAT_1.parse( date ); 358 } 359 catch ( ParseException pe2 ) 360 { 361 try 362 { 363 this.date = TIMESTAMP_FORMAT_2.parse( date ); 364 } 365 catch ( ParseException pe3 ) 366 { 367 throw new IllegalArgumentException( "Unable to parse date: " + date ); 368 } 369 } 370 } 371 } 372 } 373 else 374 { 375 try 376 { 377 this.date = TIMESTAMP_FORMAT_4.parse( date ); 378 } 379 catch ( ParseException pe1 ) 380 { 381 try 382 { 383 this.date = TIMESTAMP_FORMAT_1.parse( date ); 384 } 385 catch ( ParseException pe2 ) 386 { 387 try 388 { 389 this.date = TIMESTAMP_FORMAT_2.parse( date ); 390 } 391 catch ( ParseException pe3 ) 392 { 393 throw new IllegalArgumentException( "Unable to parse date: " + date ); 394 } 395 } 396 } 397 } 398 } 399 } 400 401 /** 402 * @return date in yyyy-mm-dd format 403 */ 404 public String getDateFormatted() 405 { 406 return DATE_FORMAT.format( getDate() ); 407 } 408 409 /** 410 * @return time in HH:mm:ss format 411 */ 412 public String getTimeFormatted() 413 { 414 return TIME_FORMAT.format( getDate() ); 415 } 416 417 /** 418 * @return 419 * @since 1.3 420 */ 421 public String getRevision() 422 { 423 return revision; 424 } 425 426 /** 427 * @param revision 428 * @since 1.3 429 */ 430 public void setRevision( String revision ) 431 { 432 this.revision = revision; 433 } 434 435 public String getParentRevision() 436 { 437 return parentRevision; 438 } 439 440 public void setParentRevision( String parentRevision ) 441 { 442 this.parentRevision = parentRevision; 443 } 444 445 public void addMergedRevision( String mergedRevision ) 446 { 447 if ( mergedRevisions == null ) 448 { 449 mergedRevisions = new LinkedHashSet<String>(); 450 } 451 mergedRevisions.add( mergedRevision ); 452 } 453 454 public Set<String> getMergedRevisions() 455 { 456 return mergedRevisions == null ? Collections.<String>emptySet() : mergedRevisions; 457 } 458 459 public void setMergedRevisions( Set<String> mergedRevisions ) 460 { 461 this.mergedRevisions = mergedRevisions; 462 } 463 464 /** {@inheritDoc} */ 465 public String toString() 466 { 467 StringBuilder result = new StringBuilder( author == null ? " null " : author ); 468 result.append( "\n" ).append( date == null ? "null " : date.toString() ).append( "\n" ); 469 // parent(s) 470 if ( parentRevision != null ) 471 { 472 result.append( "parent: " ).append( parentRevision ); 473 if ( !mergedRevisions.isEmpty() ) 474 { 475 result.append( " + " ); 476 result.append( mergedRevisions ); 477 } 478 result.append( "\n" ); 479 } 480 if ( files != null ) 481 { 482 for ( ChangeFile file : files ) 483 { 484 result.append( file == null ? " null " : file.toString() ).append( "\n" ); 485 } 486 } 487 488 result.append( comment == null ? " null " : comment ); 489 490 return result.toString(); 491 } 492 493 /** 494 * Provide the changelog entry as an XML snippet. 495 * 496 * @return a changelog-entry in xml format 497 * @task make sure comment doesn't contain CDATA tags - MAVEN114 498 */ 499 public String toXML() 500 { 501 StringBuilder buffer = new StringBuilder( "\t<changelog-entry>\n" ); 502 503 if ( getDate() != null ) 504 { 505 buffer.append( "\t\t<date pattern=\"" + DATE_PATTERN + "\">" ) 506 .append( getDateFormatted() ) 507 .append( "</date>\n" ) 508 .append( "\t\t<time pattern=\"" + TIME_PATTERN + "\">" ) 509 .append( getTimeFormatted() ) 510 .append( "</time>\n" ); 511 } 512 513 buffer.append( "\t\t<author><![CDATA[" ) 514 .append( author ) 515 .append( "]]></author>\n" ); 516 517 if ( parentRevision != null ) 518 { 519 buffer.append( "\t\t<parent>" ).append( getParentRevision() ).append( "</parent>\n" ); 520 } 521 for ( String mergedRevision : getMergedRevisions() ) 522 { 523 buffer.append( "\t\t<merge>" ).append( mergedRevision ).append( "</merge>\n" ); 524 } 525 526 if ( files != null ) 527 { 528 for ( ChangeFile file : files ) 529 { 530 buffer.append( "\t\t<file>\n" ); 531 if ( file.getAction() != null ) 532 { 533 buffer.append( "\t\t\t<action>" ).append( file.getAction() ).append( "</action>\n" ); 534 } 535 buffer.append( "\t\t\t<name>" ).append( escapeValue( file.getName() ) ).append( "</name>\n" ); 536 buffer.append( "\t\t\t<revision>" ).append( file.getRevision() ).append( "</revision>\n" ); 537 if ( file.getOriginalName() != null ) 538 { 539 buffer.append( "\t\t\t<orig-name>" ); 540 buffer.append( escapeValue( file.getOriginalName() ) ); 541 buffer.append( "</orig-name>\n" ); 542 } 543 if ( file.getOriginalRevision() != null ) 544 { 545 buffer.append( "\t\t\t<orig-revision>" ); 546 buffer.append( file.getOriginalRevision() ); 547 buffer.append( "</orig-revision>\n" ); 548 } 549 buffer.append( "\t\t</file>\n" ); 550 } 551 } 552 buffer.append( "\t\t<msg><![CDATA[" ) 553 .append( removeCDataEnd( comment ) ) 554 .append( "]]></msg>\n" ); 555 buffer.append( "\t</changelog-entry>\n" ); 556 557 return buffer.toString(); 558 } 559 560 /** {@inheritDoc} */ 561 public boolean equals( Object obj ) 562 { 563 if ( obj instanceof ChangeSet ) 564 { 565 ChangeSet changeSet = (ChangeSet) obj; 566 567 if ( toString().equals( changeSet.toString() ) ) 568 { 569 return true; 570 } 571 } 572 573 return false; 574 } 575 576 /** {@inheritDoc} */ 577 public int hashCode() 578 { 579 final int prime = 31; 580 int result = 1; 581 result = prime * result + ( ( author == null ) ? 0 : author.hashCode() ); 582 result = prime * result + ( ( comment == null ) ? 0 : comment.hashCode() ); 583 result = prime * result + ( ( date == null ) ? 0 : date.hashCode() ); 584 result = prime * result + ( ( parentRevision == null ) ? 0 : parentRevision.hashCode() ); 585 result = prime * result + ( ( mergedRevisions == null ) ? 0 : mergedRevisions.hashCode() ); 586 result = prime * result + ( ( files == null ) ? 0 : files.hashCode() ); 587 return result; 588 } 589 590 /** 591 * remove a <code>]]></code> from comments (replace it with <code>] ] ></code>). 592 * 593 * @param message The message to modify 594 * @return a clean string 595 */ 596 private String removeCDataEnd( String message ) 597 { 598 // check for invalid sequence ]]> 599 int endCdata; 600 while ( message != null && ( endCdata = message.indexOf( "]]>" ) ) > -1 ) 601 { 602 message = message.substring( 0, endCdata ) + "] ] >" + message.substring( endCdata + 3, message.length() ); 603 } 604 return message; 605 } 606 607 /** 608 * <p>Escape the <code>toString</code> of the given object. 609 * For use in an attribute value.</p> 610 * <p/> 611 * swiped from jakarta-commons/betwixt -- XMLUtils.java 612 * 613 * @param value escape <code>value.toString()</code> 614 * @return text with characters restricted (for use in attributes) escaped 615 */ 616 public static String escapeValue( Object value ) 617 { 618 StringBuilder buffer = new StringBuilder( value.toString() ); 619 for ( int i = 0, size = buffer.length(); i < size; i++ ) 620 { 621 switch ( buffer.charAt( i ) ) 622 { 623 case'<': 624 buffer.replace( i, i + 1, LESS_THAN_ENTITY ); 625 size += 3; 626 i += 3; 627 break; 628 case'>': 629 buffer.replace( i, i + 1, GREATER_THAN_ENTITY ); 630 size += 3; 631 i += 3; 632 break; 633 case'&': 634 buffer.replace( i, i + 1, AMPERSAND_ENTITY ); 635 size += 4; 636 i += 4; 637 break; 638 case'\'': 639 buffer.replace( i, i + 1, APOSTROPHE_ENTITY ); 640 size += 5; 641 i += 5; 642 break; 643 case'\"': 644 buffer.replace( i, i + 1, QUOTE_ENTITY ); 645 size += 5; 646 i += 5; 647 break; 648 default: 649 } 650 } 651 return buffer.toString(); 652 } 653}