View Javadoc
1   package org.apache.maven.scm;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   * http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.Serializable;
23  import java.text.ParseException;
24  import java.text.SimpleDateFormat;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.Date;
28  import java.util.LinkedHashSet;
29  import java.util.List;
30  import java.util.Set;
31  
32  import org.apache.maven.scm.provider.ScmProviderRepository;
33  import org.apache.maven.scm.util.FilenameUtils;
34  import org.apache.maven.scm.util.ThreadSafeDateFormat;
35  import org.codehaus.plexus.util.StringUtils;
36  
37  /**
38   * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
39   *
40   */
41  public class ChangeSet
42      implements Serializable
43  {
44      /**
45       *
46       */
47      private static final long serialVersionUID = 7097705862222539801L;
48  
49      /**
50       * Escaped <code>&lt;</code> entity
51       */
52      public static final String LESS_THAN_ENTITY = "&lt;";
53  
54      /**
55       * Escaped <code>&gt;</code> entity
56       */
57      public static final String GREATER_THAN_ENTITY = "&gt;";
58  
59      /**
60       * Escaped <code>&amp;</code> entity
61       */
62      public static final String AMPERSAND_ENTITY = "&amp;";
63  
64      /**
65       * Escaped <code>'</code> entity
66       */
67      public static final String APOSTROPHE_ENTITY = "&apos;";
68  
69      /**
70       * Escaped <code>"</code> entity
71       */
72      public static final String QUOTE_ENTITY = "&quot;";
73  
74      private static final String DATE_PATTERN = "yyyy-MM-dd";
75  
76      /**
77       * Formatter used by the getDateFormatted method.
78       */
79      private static final ThreadSafeDateFormat DATE_FORMAT = new ThreadSafeDateFormat( DATE_PATTERN );
80  
81      private static final String TIME_PATTERN = "HH:mm:ss";
82  
83      /**
84       * Formatter used by the getTimeFormatted method.
85       */
86      private static final ThreadSafeDateFormat TIME_FORMAT = new ThreadSafeDateFormat( TIME_PATTERN );
87  
88      /**
89       * Formatter used to parse date/timestamp.
90       */
91      private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_1 = new ThreadSafeDateFormat( "yyyy/MM/dd HH:mm:ss" );
92  
93      private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_2 = new ThreadSafeDateFormat( "yyyy-MM-dd HH:mm:ss" );
94  
95      private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_3 = new ThreadSafeDateFormat( "yyyy/MM/dd HH:mm:ss z" );
96  
97      private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_4 = new ThreadSafeDateFormat( "yyyy-MM-dd HH:mm:ss z" );
98  
99      /**
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>" ).append( escapeValue( file.getOriginalName() ) ).append( "</orig-name>\n" );
540                 }
541                 if ( file.getOriginalRevision() != null )
542                 {
543                     buffer.append( "\t\t\t<orig-revision>" ).append( file.getOriginalRevision() ).append( "</orig-revision>\n" );
544                 }
545                 buffer.append( "\t\t</file>\n" );
546             }
547         }
548         buffer.append( "\t\t<msg><![CDATA[" )
549             .append( removeCDataEnd( comment ) )
550             .append( "]]></msg>\n" );
551         buffer.append( "\t</changelog-entry>\n" );
552 
553         return buffer.toString();
554     }
555 
556     /** {@inheritDoc} */
557     public boolean equals( Object obj )
558     {
559         if ( obj instanceof ChangeSet )
560         {
561             ChangeSet changeSet = (ChangeSet) obj;
562 
563             if ( toString().equals( changeSet.toString() ) )
564             {
565                 return true;
566             }
567         }
568 
569         return false;
570     }
571 
572     /** {@inheritDoc} */
573     public int hashCode()
574     {
575         final int prime = 31;
576         int result = 1;
577         result = prime * result + ( ( author == null ) ? 0 : author.hashCode() );
578         result = prime * result + ( ( comment == null ) ? 0 : comment.hashCode() );
579         result = prime * result + ( ( date == null ) ? 0 : date.hashCode() );
580         result = prime * result + ( ( parentRevision == null ) ? 0 : parentRevision.hashCode() );
581         result = prime * result + ( ( mergedRevisions == null ) ? 0 : mergedRevisions.hashCode() );
582         result = prime * result + ( ( files == null ) ? 0 : files.hashCode() );
583         return result;
584     }
585 
586     /**
587      * remove a <code>]]></code> from comments (replace it with <code>] ] ></code>).
588      *
589      * @param message The message to modify
590      * @return a clean string
591      */
592     private String removeCDataEnd( String message )
593     {
594         // check for invalid sequence ]]>
595         int endCdata;
596         while ( message != null && ( endCdata = message.indexOf( "]]>" ) ) > -1 )
597         {
598             message = message.substring( 0, endCdata ) + "] ] >" + message.substring( endCdata + 3, message.length() );
599         }
600         return message;
601     }
602 
603     /**
604      * <p>Escape the <code>toString</code> of the given object.
605      * For use in an attribute value.</p>
606      * <p/>
607      * swiped from jakarta-commons/betwixt -- XMLUtils.java
608      *
609      * @param value escape <code>value.toString()</code>
610      * @return text with characters restricted (for use in attributes) escaped
611      */
612     public static String escapeValue( Object value )
613     {
614         StringBuilder buffer = new StringBuilder( value.toString() );
615         for ( int i = 0, size = buffer.length(); i < size; i++ )
616         {
617             switch ( buffer.charAt( i ) )
618             {
619                 case'<':
620                     buffer.replace( i, i + 1, LESS_THAN_ENTITY );
621                     size += 3;
622                     i += 3;
623                     break;
624                 case'>':
625                     buffer.replace( i, i + 1, GREATER_THAN_ENTITY );
626                     size += 3;
627                     i += 3;
628                     break;
629                 case'&':
630                     buffer.replace( i, i + 1, AMPERSAND_ENTITY );
631                     size += 4;
632                     i += 4;
633                     break;
634                 case'\'':
635                     buffer.replace( i, i + 1, APOSTROPHE_ENTITY );
636                     size += 5;
637                     i += 5;
638                     break;
639                 case'\"':
640                     buffer.replace( i, i + 1, QUOTE_ENTITY );
641                     size += 5;
642                     i += 5;
643                     break;
644             }
645         }
646         return buffer.toString();
647     }
648 }