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      * List of tags
121      */
122     private List<String> tags;
123 
124     /**
125      * The SCM revision id for this changeset.
126      * @since 1.3
127      */
128     private String revision;
129 
130     /**
131      * Revision from which this one originates
132      * @since 1.7
133      */
134     private String parentRevision;
135 
136     /**
137      * Revisions that were merged into this one
138      * @since 1.7
139      */
140     private Set<String> mergedRevisions;
141 
142     /**
143      * @param strDate         Date the changes were committed
144      * @param userDatePattern pattern of date
145      * @param comment         comment provided at commit time
146      * @param author          User who made changes
147      * @param files           The ChangeFile list
148      */
149     public ChangeSet( String strDate, String userDatePattern, String comment, String author,
150                       List<ChangeFile> files )
151     {
152         this( null, comment, author, files );
153 
154         setDate( strDate, userDatePattern );
155     }
156 
157     /**
158      * @param date    Date the changes were committed
159      * @param comment comment provided at commit time
160      * @param author  User who made changes
161      * @param files   The ChangeFile list
162      */
163     public ChangeSet( Date date, String comment, String author, List<ChangeFile> files )
164     {
165         setDate( date );
166 
167         setAuthor( author );
168 
169         setComment( comment );
170 
171         this.files = files;
172     }
173 
174     /**
175      * Constructor used when attributes aren't available until later
176      */
177     public ChangeSet()
178     {
179         // no op
180     }
181 
182     /**
183      * Getter for ChangeFile list.
184      *
185      * @return List of ChangeFile.
186      */
187     public List<ChangeFile> getFiles()
188     {
189         if ( files == null )
190         {
191             return new ArrayList<ChangeFile>();
192         }
193         return files;
194     }
195 
196     /**
197      * Setter for ChangeFile list.
198      *
199      * @param files List of ChangeFiles.
200      */
201     public void setFiles( List<ChangeFile> files )
202     {
203         this.files = files;
204     }
205 
206     public void addFile( ChangeFile file )
207     {
208         if ( files == null )
209         {
210             files = new ArrayList<ChangeFile>();
211         }
212 
213         files.add( file );
214     }
215 
216     /**
217      * @deprecated Use method {@link #containsFilename(String)}
218      * @param filename TODO
219      * @param repository NOT USED
220      * @return TODO
221      */
222     public boolean containsFilename( String filename, ScmProviderRepository repository )
223     {
224         return containsFilename( filename );
225     }
226 
227     public boolean containsFilename( String filename )
228     {
229         if ( files != null )
230         {
231             for ( ChangeFile file : files )
232             {
233                 String f1 = FilenameUtils.normalizeFilename( file.getName() );
234                 String f2 = FilenameUtils.normalizeFilename( filename );
235                 if ( f1.indexOf( f2 ) >= 0 )
236                 {
237                     return true;
238                 }
239             }
240         }
241 
242         return false;
243     }
244 
245     /**
246      * Getter for property author.
247      *
248      * @return Value of property author.
249      */
250     public String getAuthor()
251     {
252         return author;
253     }
254 
255     /**
256      * Setter for property author.
257      *
258      * @param author New value of property author.
259      */
260     public void setAuthor( String author )
261     {
262         this.author = author;
263     }
264 
265     /**
266      * Getter for property comment.
267      *
268      * @return Value of property comment.
269      */
270     public String getComment()
271     {
272         return comment;
273     }
274 
275     /**
276      * Setter for property comment.
277      *
278      * @param comment New value of property comment.
279      */
280     public void setComment( String comment )
281     {
282         this.comment = comment;
283     }
284 
285     /**
286      * Getter for property date.
287      *
288      * @return Value of property date.
289      */
290     public Date getDate()
291     {
292         if ( date != null )
293         {
294             return (Date) date.clone();
295         }
296 
297         return null;
298     }
299 
300     /**
301      * Setter for property date.
302      *
303      * @param date New value of property date.
304      */
305     public void setDate( Date date )
306     {
307         if ( date != null )
308         {
309             this.date = new Date( date.getTime() );
310         }
311     }
312 
313     /**
314      * Setter for property date that takes a string and parses it
315      *
316      * @param date - a string in yyyy/MM/dd HH:mm:ss format
317      */
318     public void setDate( String date )
319     {
320         setDate( date, null );
321     }
322 
323     /**
324      * Setter for property date that takes a string and parses it
325      *
326      * @param date            - a string in yyyy/MM/dd HH:mm:ss format
327      * @param userDatePattern - pattern of date
328      */
329     public void setDate( String date, String userDatePattern )
330     {
331         try
332         {
333             if ( !StringUtils.isEmpty( userDatePattern ) )
334             {
335                 SimpleDateFormat format = new SimpleDateFormat( userDatePattern );
336 
337                 this.date = format.parse( date );
338             }
339             else
340             {
341                 this.date = TIMESTAMP_FORMAT_3.parse( date );
342             }
343         }
344         catch ( ParseException e )
345         {
346             if ( !StringUtils.isEmpty( userDatePattern ) )
347             {
348                 try
349                 {
350                     this.date = TIMESTAMP_FORMAT_3.parse( date );
351                 }
352                 catch ( ParseException pe )
353                 {
354                     try
355                     {
356                         this.date = TIMESTAMP_FORMAT_4.parse( date );
357                     }
358                     catch ( ParseException pe1 )
359                     {
360                         try
361                         {
362                             this.date = TIMESTAMP_FORMAT_1.parse( date );
363                         }
364                         catch ( ParseException pe2 )
365                         {
366                             try
367                             {
368                                 this.date = TIMESTAMP_FORMAT_2.parse( date );
369                             }
370                             catch ( ParseException pe3 )
371                             {
372                                 throw new IllegalArgumentException( "Unable to parse date: " + date );
373                             }
374                         }
375                     }
376                 }
377             }
378             else
379             {
380                 try
381                 {
382                     this.date = TIMESTAMP_FORMAT_4.parse( date );
383                 }
384                 catch ( ParseException pe1 )
385                 {
386                     try
387                     {
388                         this.date = TIMESTAMP_FORMAT_1.parse( date );
389                     }
390                     catch ( ParseException pe2 )
391                     {
392                         try
393                         {
394                             this.date = TIMESTAMP_FORMAT_2.parse( date );
395                         }
396                         catch ( ParseException pe3 )
397                         {
398                             throw new IllegalArgumentException( "Unable to parse date: " + date );
399                         }
400                     }
401                 }
402             }
403         }
404     }
405 
406     /**
407      * @return date in yyyy-mm-dd format
408      */
409     public String getDateFormatted()
410     {
411         return DATE_FORMAT.format( getDate() );
412     }
413 
414     /**
415      * @return time in HH:mm:ss format
416      */
417     public String getTimeFormatted()
418     {
419         return TIME_FORMAT.format( getDate() );
420     }
421 
422     /**
423      * Getter for property tags.
424      *
425      * @return Value of property author.
426      */
427     public List<String> getTags()
428     {
429         if ( tags == null )
430         {
431             return new ArrayList<>();
432         }
433         return tags;
434     }
435 
436     /**
437      * Setter for property tags.
438      *
439      * @param tags New value of property tags. This replaces the existing list (if any).
440      */
441     public void setTags( List<String> tags )
442     {
443         this.tags = tags;
444     }
445 
446     /**
447      * Setter for property tags.
448      *
449      * @param tag New tag to add to the list of tags.
450      */
451     public void addTag( String tag )
452     {
453         if ( tag == null )
454         {
455             return;
456         }
457         tag = tag.trim();
458         if ( tag.isEmpty() )
459         {
460             return;
461         }
462         if ( tags == null )
463         {
464             tags = new ArrayList<>();
465         }
466         tags.add( tag );
467     }
468 
469     /**
470      * @return TODO
471      * @since 1.3
472      */
473     public String getRevision()
474     {
475         return revision;
476     }
477 
478     /**
479      * @param revision TODO
480      * @since 1.3
481      */
482     public void setRevision( String revision )
483     {
484         this.revision = revision;
485     }
486 
487     public String getParentRevision()
488     {
489         return parentRevision;
490     }
491 
492     public void setParentRevision( String parentRevision )
493     {
494         this.parentRevision = parentRevision;
495     }
496 
497     public void addMergedRevision( String mergedRevision )
498     {
499         if ( mergedRevisions == null )
500         {
501             mergedRevisions = new LinkedHashSet<String>();
502         }
503         mergedRevisions.add( mergedRevision );
504     }
505 
506     public Set<String> getMergedRevisions()
507     {
508         return mergedRevisions == null ? Collections.<String>emptySet() : mergedRevisions;
509     }
510 
511     public void setMergedRevisions( Set<String> mergedRevisions )
512     {
513         this.mergedRevisions = mergedRevisions;
514     }
515 
516     /** {@inheritDoc} */
517     public String toString()
518     {
519         StringBuilder result = new StringBuilder( author == null ? " null " : author );
520         result.append( "\n" ).append( date == null ? "null " : date.toString() ).append( "\n" );
521         List<String> tags = getTags();
522         if ( !tags.isEmpty() )
523         {
524             result.append( "tags:" ).append( tags ).append( "\n" );
525         }
526         // parent(s)
527         if ( parentRevision != null )
528         {
529             result.append( "parent: " ).append( parentRevision );
530             if ( !getMergedRevisions().isEmpty() )
531             {
532                 result.append( " + " );
533                 result.append( getMergedRevisions() );
534             }
535             result.append( "\n" );
536         }
537         if ( files != null )
538         {
539             for ( ChangeFile file : files )
540             {
541                 result.append( file == null ? " null " : file.toString() ).append( "\n" );
542             }
543         }
544 
545         result.append( comment == null ? " null " : comment );
546 
547         return result.toString();
548     }
549 
550     /**
551      * Provide the changelog entry as an XML snippet.
552      *
553      * @return a changelog-entry in xml format
554      * TODO make sure comment doesn't contain CDATA tags - MAVEN114
555      */
556     public String toXML()
557     {
558         StringBuilder buffer = new StringBuilder( "\t<changelog-entry>\n" );
559 
560         if ( getDate() != null )
561         {
562             buffer.append( "\t\t<date pattern=\"" + DATE_PATTERN + "\">" )
563                 .append( getDateFormatted() )
564                 .append( "</date>\n" )
565                 .append( "\t\t<time pattern=\"" + TIME_PATTERN + "\">" )
566                 .append( getTimeFormatted() )
567                 .append( "</time>\n" );
568         }
569 
570         buffer.append( "\t\t<author><![CDATA[" )
571             .append( author )
572             .append( "]]></author>\n" );
573 
574         if ( parentRevision != null )
575         {
576             buffer.append( "\t\t<parent>" ).append( getParentRevision() ).append( "</parent>\n" );
577         }
578         for ( String mergedRevision : getMergedRevisions() )
579         {
580             buffer.append( "\t\t<merge>" ).append( mergedRevision ).append( "</merge>\n" );
581         }
582 
583         if ( files != null )
584         {
585             for ( ChangeFile file : files )
586             {
587                 buffer.append( "\t\t<file>\n" );
588                 if ( file.getAction() != null )
589                 {
590                     buffer.append( "\t\t\t<action>" ).append( file.getAction() ).append( "</action>\n" );
591                 }
592                 buffer.append( "\t\t\t<name>" ).append( escapeValue( file.getName() ) ).append( "</name>\n" );
593                 buffer.append( "\t\t\t<revision>" ).append( file.getRevision() ).append( "</revision>\n" );
594                 if ( file.getOriginalName() != null )
595                 {
596                     buffer.append( "\t\t\t<orig-name>" );
597                     buffer.append( escapeValue( file.getOriginalName() ) );
598                     buffer.append( "</orig-name>\n" );
599                 }
600                 if ( file.getOriginalRevision() != null )
601                 {
602                     buffer.append( "\t\t\t<orig-revision>" );
603                     buffer.append( file.getOriginalRevision() );
604                     buffer.append( "</orig-revision>\n" );
605                 }
606                 buffer.append( "\t\t</file>\n" );
607             }
608         }
609         buffer.append( "\t\t<msg><![CDATA[" )
610             .append( removeCDataEnd( comment ) )
611             .append( "]]></msg>\n" );
612         List<String> tags = getTags();
613         if ( !tags.isEmpty() )
614         {
615             buffer.append( "\t\t<tags>\n" );
616             for ( String tag: tags )
617             {
618                 buffer.append( "\t\t\t<tag>" ).append( escapeValue( tag ) ).append( "</tag>\n" );
619             }
620             buffer.append( "\t\t</tags>\n" );
621         }
622         buffer.append( "\t</changelog-entry>\n" );
623 
624         return buffer.toString();
625     }
626 
627     /** {@inheritDoc} */
628     public boolean equals( Object obj )
629     {
630         if ( obj instanceof ChangeSet )
631         {
632             ChangeSet changeSet = (ChangeSet) obj;
633 
634             if ( toString().equals( changeSet.toString() ) )
635             {
636                 return true;
637             }
638         }
639 
640         return false;
641     }
642 
643     /** {@inheritDoc} */
644     public int hashCode()
645     {
646         final int prime = 31;
647         int result = 1;
648         result = prime * result + ( ( author == null ) ? 0 : author.hashCode() );
649         result = prime * result + ( ( comment == null ) ? 0 : comment.hashCode() );
650         result = prime * result + ( ( date == null ) ? 0 : date.hashCode() );
651         result = prime * result + ( ( parentRevision == null ) ? 0 : parentRevision.hashCode() );
652         result = prime * result + ( ( mergedRevisions == null ) ? 0 : mergedRevisions.hashCode() );
653         result = prime * result + ( ( files == null ) ? 0 : files.hashCode() );
654         return result;
655     }
656 
657     /**
658      * remove a <code>]]></code> from comments (replace it with <code>] ] ></code>).
659      *
660      * @param message The message to modify
661      * @return a clean string
662      */
663     private String removeCDataEnd( String message )
664     {
665         // check for invalid sequence ]]>
666         int endCdata;
667         while ( message != null && ( endCdata = message.indexOf( "]]>" ) ) > -1 )
668         {
669             message = message.substring( 0, endCdata ) + "] ] >" + message.substring( endCdata + 3, message.length() );
670         }
671         return message;
672     }
673 
674     /**
675      * <p>Escape the <code>toString</code> of the given object.
676      * For use in an attribute value.</p>
677      * <p>
678      * swiped from jakarta-commons/betwixt -- XMLUtils.java
679      *
680      * @param value escape <code>value.toString()</code>
681      * @return text with characters restricted (for use in attributes) escaped
682      */
683     public static String escapeValue( Object value )
684     {
685         StringBuilder buffer = new StringBuilder( value.toString() );
686         for ( int i = 0, size = buffer.length(); i < size; i++ )
687         {
688             switch ( buffer.charAt( i ) )
689             {
690                 case'<':
691                     buffer.replace( i, i + 1, LESS_THAN_ENTITY );
692                     size += 3;
693                     i += 3;
694                     break;
695                 case'>':
696                     buffer.replace( i, i + 1, GREATER_THAN_ENTITY );
697                     size += 3;
698                     i += 3;
699                     break;
700                 case'&':
701                     buffer.replace( i, i + 1, AMPERSAND_ENTITY );
702                     size += 4;
703                     i += 4;
704                     break;
705                 case'\'':
706                     buffer.replace( i, i + 1, APOSTROPHE_ENTITY );
707                     size += 5;
708                     i += 5;
709                     break;
710                 case'\"':
711                     buffer.replace( i, i + 1, QUOTE_ENTITY );
712                     size += 5;
713                     i += 5;
714                     break;
715                 default:
716             }
717         }
718         return buffer.toString();
719     }
720 }