View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.scm;
20  
21  import java.io.Serializable;
22  import java.text.ParseException;
23  import java.text.SimpleDateFormat;
24  import java.util.ArrayList;
25  import java.util.Collections;
26  import java.util.Date;
27  import java.util.LinkedHashSet;
28  import java.util.List;
29  import java.util.Set;
30  
31  import org.apache.maven.scm.provider.ScmProviderRepository;
32  import org.apache.maven.scm.util.FilenameUtils;
33  import org.apache.maven.scm.util.ThreadSafeDateFormat;
34  import org.codehaus.plexus.util.StringUtils;
35  
36  /**
37   * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
38   *
39   */
40  public class ChangeSet implements Serializable {
41      /**
42       *
43       */
44      private static final long serialVersionUID = 7097705862222539801L;
45  
46      /**
47       * Escaped <code>&lt;</code> entity
48       */
49      public static final String LESS_THAN_ENTITY = "&lt;";
50  
51      /**
52       * Escaped <code>&gt;</code> entity
53       */
54      public static final String GREATER_THAN_ENTITY = "&gt;";
55  
56      /**
57       * Escaped <code>&amp;</code> entity
58       */
59      public static final String AMPERSAND_ENTITY = "&amp;";
60  
61      /**
62       * Escaped <code>'</code> entity
63       */
64      public static final String APOSTROPHE_ENTITY = "&apos;";
65  
66      /**
67       * Escaped <code>"</code> entity
68       */
69      public static final String QUOTE_ENTITY = "&quot;";
70  
71      private static final String DATE_PATTERN = "yyyy-MM-dd";
72  
73      /**
74       * Formatter used by the getDateFormatted method.
75       */
76      private static final ThreadSafeDateFormat DATE_FORMAT = new ThreadSafeDateFormat(DATE_PATTERN);
77  
78      private static final String TIME_PATTERN = "HH:mm:ss";
79  
80      /**
81       * Formatter used by the getTimeFormatted method.
82       */
83      private static final ThreadSafeDateFormat TIME_FORMAT = new ThreadSafeDateFormat(TIME_PATTERN);
84  
85      /**
86       * Formatter used to parse date/timestamp.
87       */
88      private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_1 = new ThreadSafeDateFormat("yyyy/MM/dd HH:mm:ss");
89  
90      private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_2 = new ThreadSafeDateFormat("yyyy-MM-dd HH:mm:ss");
91  
92      private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_3 = new ThreadSafeDateFormat("yyyy/MM/dd HH:mm:ss z");
93  
94      private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_4 = new ThreadSafeDateFormat("yyyy-MM-dd HH:mm:ss z");
95  
96      /**
97       * Date the changes were committed
98       */
99      private Date date;
100 
101     /**
102      * User who made changes
103      */
104     private String author;
105 
106     /**
107      * comment provided at commit time
108      */
109     private String comment = "";
110 
111     /**
112      * List of ChangeFile
113      */
114     private List<ChangeFile> files;
115 
116     /**
117      * List of tags
118      */
119     private List<String> tags;
120 
121     /**
122      * The SCM revision id for this changeset.
123      * @since 1.3
124      */
125     private String revision;
126 
127     /**
128      * Revision from which this one originates
129      * @since 1.7
130      */
131     private String parentRevision;
132 
133     /**
134      * Revisions that were merged into this one
135      * @since 1.7
136      */
137     private Set<String> mergedRevisions;
138 
139     /**
140      * @param strDate         Date the changes were committed
141      * @param userDatePattern pattern of date
142      * @param comment         comment provided at commit time
143      * @param author          User who made changes
144      * @param files           The ChangeFile list
145      */
146     public ChangeSet(String strDate, String userDatePattern, String comment, String author, List<ChangeFile> files) {
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         setDate(date);
160 
161         setAuthor(author);
162 
163         setComment(comment);
164 
165         this.files = files;
166     }
167 
168     /**
169      * Constructor used when attributes aren't available until later
170      */
171     public ChangeSet() {
172         // no op
173     }
174 
175     /**
176      * Getter for ChangeFile list.
177      *
178      * @return List of ChangeFile.
179      */
180     public List<ChangeFile> getFiles() {
181         if (files == null) {
182             return new ArrayList<ChangeFile>();
183         }
184         return files;
185     }
186 
187     /**
188      * Setter for ChangeFile list.
189      *
190      * @param files List of ChangeFiles.
191      */
192     public void setFiles(List<ChangeFile> files) {
193         this.files = files;
194     }
195 
196     public void addFile(ChangeFile file) {
197         if (files == null) {
198             files = new ArrayList<ChangeFile>();
199         }
200 
201         files.add(file);
202     }
203 
204     /**
205      * @deprecated Use method {@link #containsFilename(String)}
206      * @param filename TODO
207      * @param repository NOT USED
208      * @return TODO
209      */
210     public boolean containsFilename(String filename, ScmProviderRepository repository) {
211         return containsFilename(filename);
212     }
213 
214     public boolean containsFilename(String filename) {
215         if (files != null) {
216             for (ChangeFile file : files) {
217                 String f1 = FilenameUtils.normalizeFilename(file.getName());
218                 String f2 = FilenameUtils.normalizeFilename(filename);
219                 if (f1.indexOf(f2) >= 0) {
220                     return true;
221                 }
222             }
223         }
224 
225         return false;
226     }
227 
228     /**
229      * Getter for property author.
230      *
231      * @return Value of property author.
232      */
233     public String getAuthor() {
234         return author;
235     }
236 
237     /**
238      * Setter for property author.
239      *
240      * @param author New value of property author.
241      */
242     public void setAuthor(String author) {
243         this.author = author;
244     }
245 
246     /**
247      * Getter for property comment.
248      *
249      * @return Value of property comment.
250      */
251     public String getComment() {
252         return comment;
253     }
254 
255     /**
256      * Setter for property comment.
257      *
258      * @param comment New value of property comment.
259      */
260     public void setComment(String comment) {
261         this.comment = comment;
262     }
263 
264     /**
265      * Getter for property date.
266      *
267      * @return Value of property date.
268      */
269     public Date getDate() {
270         if (date != null) {
271             return (Date) date.clone();
272         }
273 
274         return null;
275     }
276 
277     /**
278      * Setter for property date.
279      *
280      * @param date New value of property date.
281      */
282     public void setDate(Date date) {
283         if (date != null) {
284             this.date = new Date(date.getTime());
285         }
286     }
287 
288     /**
289      * Setter for property date that takes a string and parses it
290      *
291      * @param date - a string in yyyy/MM/dd HH:mm:ss format
292      */
293     public void setDate(String date) {
294         setDate(date, null);
295     }
296 
297     /**
298      * Setter for property date that takes a string and parses it
299      *
300      * @param date            - a string in yyyy/MM/dd HH:mm:ss format
301      * @param userDatePattern - pattern of date
302      */
303     public void setDate(String date, String userDatePattern) {
304         try {
305             if (!StringUtils.isEmpty(userDatePattern)) {
306                 SimpleDateFormat format = new SimpleDateFormat(userDatePattern);
307 
308                 this.date = format.parse(date);
309             } else {
310                 this.date = TIMESTAMP_FORMAT_3.parse(date);
311             }
312         } catch (ParseException e) {
313             if (!StringUtils.isEmpty(userDatePattern)) {
314                 try {
315                     this.date = TIMESTAMP_FORMAT_3.parse(date);
316                 } catch (ParseException pe) {
317                     try {
318                         this.date = TIMESTAMP_FORMAT_4.parse(date);
319                     } catch (ParseException pe1) {
320                         try {
321                             this.date = TIMESTAMP_FORMAT_1.parse(date);
322                         } catch (ParseException pe2) {
323                             try {
324                                 this.date = TIMESTAMP_FORMAT_2.parse(date);
325                             } catch (ParseException pe3) {
326                                 throw new IllegalArgumentException("Unable to parse date: " + date);
327                             }
328                         }
329                     }
330                 }
331             } else {
332                 try {
333                     this.date = TIMESTAMP_FORMAT_4.parse(date);
334                 } catch (ParseException pe1) {
335                     try {
336                         this.date = TIMESTAMP_FORMAT_1.parse(date);
337                     } catch (ParseException pe2) {
338                         try {
339                             this.date = TIMESTAMP_FORMAT_2.parse(date);
340                         } catch (ParseException pe3) {
341                             throw new IllegalArgumentException("Unable to parse date: " + date);
342                         }
343                     }
344                 }
345             }
346         }
347     }
348 
349     /**
350      * @return date in yyyy-mm-dd format
351      */
352     public String getDateFormatted() {
353         return DATE_FORMAT.format(getDate());
354     }
355 
356     /**
357      * @return time in HH:mm:ss format
358      */
359     public String getTimeFormatted() {
360         return TIME_FORMAT.format(getDate());
361     }
362 
363     /**
364      * Getter for property tags.
365      *
366      * @return Value of property author.
367      */
368     public List<String> getTags() {
369         if (tags == null) {
370             return new ArrayList<>();
371         }
372         return tags;
373     }
374 
375     /**
376      * Setter for property tags.
377      *
378      * @param tags New value of property tags. This replaces the existing list (if any).
379      */
380     public void setTags(List<String> tags) {
381         this.tags = tags;
382     }
383 
384     /**
385      * Setter for property tags.
386      *
387      * @param tag New tag to add to the list of tags.
388      */
389     public void addTag(String tag) {
390         if (tag == null) {
391             return;
392         }
393         tag = tag.trim();
394         if (tag.isEmpty()) {
395             return;
396         }
397         if (tags == null) {
398             tags = new ArrayList<>();
399         }
400         tags.add(tag);
401     }
402 
403     /**
404      * @return TODO
405      * @since 1.3
406      */
407     public String getRevision() {
408         return revision;
409     }
410 
411     /**
412      * @param revision TODO
413      * @since 1.3
414      */
415     public void setRevision(String revision) {
416         this.revision = revision;
417     }
418 
419     public String getParentRevision() {
420         return parentRevision;
421     }
422 
423     public void setParentRevision(String parentRevision) {
424         this.parentRevision = parentRevision;
425     }
426 
427     public void addMergedRevision(String mergedRevision) {
428         if (mergedRevisions == null) {
429             mergedRevisions = new LinkedHashSet<String>();
430         }
431         mergedRevisions.add(mergedRevision);
432     }
433 
434     public Set<String> getMergedRevisions() {
435         return mergedRevisions == null ? Collections.<String>emptySet() : mergedRevisions;
436     }
437 
438     public void setMergedRevisions(Set<String> mergedRevisions) {
439         this.mergedRevisions = mergedRevisions;
440     }
441 
442     /** {@inheritDoc} */
443     public String toString() {
444         StringBuilder result = new StringBuilder(author == null ? " null " : author);
445         result.append("\n").append(date == null ? "null " : date.toString()).append("\n");
446         List<String> tags = getTags();
447         if (!tags.isEmpty()) {
448             result.append("tags:").append(tags).append("\n");
449         }
450         // parent(s)
451         if (parentRevision != null) {
452             result.append("parent: ").append(parentRevision);
453             if (!getMergedRevisions().isEmpty()) {
454                 result.append(" + ");
455                 result.append(getMergedRevisions());
456             }
457             result.append("\n");
458         }
459         if (files != null) {
460             for (ChangeFile file : files) {
461                 result.append(file == null ? " null " : file.toString()).append("\n");
462             }
463         }
464 
465         result.append(comment == null ? " null " : comment);
466 
467         return result.toString();
468     }
469 
470     /**
471      * Provide the changelog entry as an XML snippet.
472      *
473      * @return a changelog-entry in xml format
474      * TODO make sure comment doesn't contain CDATA tags - MAVEN114
475      */
476     public String toXML() {
477         StringBuilder buffer = new StringBuilder("\t<changelog-entry>\n");
478 
479         if (getDate() != null) {
480             buffer.append("\t\t<date pattern=\"" + DATE_PATTERN + "\">")
481                     .append(getDateFormatted())
482                     .append("</date>\n")
483                     .append("\t\t<time pattern=\"" + TIME_PATTERN + "\">")
484                     .append(getTimeFormatted())
485                     .append("</time>\n");
486         }
487 
488         buffer.append("\t\t<author><![CDATA[").append(author).append("]]></author>\n");
489 
490         if (parentRevision != null) {
491             buffer.append("\t\t<parent>").append(getParentRevision()).append("</parent>\n");
492         }
493         for (String mergedRevision : getMergedRevisions()) {
494             buffer.append("\t\t<merge>").append(mergedRevision).append("</merge>\n");
495         }
496 
497         if (files != null) {
498             for (ChangeFile file : files) {
499                 buffer.append("\t\t<file>\n");
500                 if (file.getAction() != null) {
501                     buffer.append("\t\t\t<action>").append(file.getAction()).append("</action>\n");
502                 }
503                 buffer.append("\t\t\t<name>")
504                         .append(escapeValue(file.getName()))
505                         .append("</name>\n");
506                 buffer.append("\t\t\t<revision>").append(file.getRevision()).append("</revision>\n");
507                 if (file.getOriginalName() != null) {
508                     buffer.append("\t\t\t<orig-name>");
509                     buffer.append(escapeValue(file.getOriginalName()));
510                     buffer.append("</orig-name>\n");
511                 }
512                 if (file.getOriginalRevision() != null) {
513                     buffer.append("\t\t\t<orig-revision>");
514                     buffer.append(file.getOriginalRevision());
515                     buffer.append("</orig-revision>\n");
516                 }
517                 buffer.append("\t\t</file>\n");
518             }
519         }
520         buffer.append("\t\t<msg><![CDATA[").append(removeCDataEnd(comment)).append("]]></msg>\n");
521         List<String> tags = getTags();
522         if (!tags.isEmpty()) {
523             buffer.append("\t\t<tags>\n");
524             for (String tag : tags) {
525                 buffer.append("\t\t\t<tag>").append(escapeValue(tag)).append("</tag>\n");
526             }
527             buffer.append("\t\t</tags>\n");
528         }
529         buffer.append("\t</changelog-entry>\n");
530 
531         return buffer.toString();
532     }
533 
534     /** {@inheritDoc} */
535     public boolean equals(Object obj) {
536         if (obj instanceof ChangeSet) {
537             ChangeSet changeSet = (ChangeSet) obj;
538 
539             if (toString().equals(changeSet.toString())) {
540                 return true;
541             }
542         }
543 
544         return false;
545     }
546 
547     /** {@inheritDoc} */
548     public int hashCode() {
549         final int prime = 31;
550         int result = 1;
551         result = prime * result + ((author == null) ? 0 : author.hashCode());
552         result = prime * result + ((comment == null) ? 0 : comment.hashCode());
553         result = prime * result + ((date == null) ? 0 : date.hashCode());
554         result = prime * result + ((parentRevision == null) ? 0 : parentRevision.hashCode());
555         result = prime * result + ((mergedRevisions == null) ? 0 : mergedRevisions.hashCode());
556         result = prime * result + ((files == null) ? 0 : files.hashCode());
557         return result;
558     }
559 
560     /**
561      * remove a <code>]]></code> from comments (replace it with <code>] ] ></code>).
562      *
563      * @param message The message to modify
564      * @return a clean string
565      */
566     private String removeCDataEnd(String message) {
567         // check for invalid sequence ]]>
568         int endCdata;
569         while (message != null && (endCdata = message.indexOf("]]>")) > -1) {
570             message = message.substring(0, endCdata) + "] ] >" + message.substring(endCdata + 3, message.length());
571         }
572         return message;
573     }
574 
575     /**
576      * <p>Escape the <code>toString</code> of the given object.
577      * For use in an attribute value.</p>
578      * <p>
579      * swiped from jakarta-commons/betwixt -- XMLUtils.java
580      *
581      * @param value escape <code>value.toString()</code>
582      * @return text with characters restricted (for use in attributes) escaped
583      */
584     public static String escapeValue(Object value) {
585         StringBuilder buffer = new StringBuilder(value.toString());
586         for (int i = 0, size = buffer.length(); i < size; i++) {
587             switch (buffer.charAt(i)) {
588                 case '<':
589                     buffer.replace(i, i + 1, LESS_THAN_ENTITY);
590                     size += 3;
591                     i += 3;
592                     break;
593                 case '>':
594                     buffer.replace(i, i + 1, GREATER_THAN_ENTITY);
595                     size += 3;
596                     i += 3;
597                     break;
598                 case '&':
599                     buffer.replace(i, i + 1, AMPERSAND_ENTITY);
600                     size += 4;
601                     i += 4;
602                     break;
603                 case '\'':
604                     buffer.replace(i, i + 1, APOSTROPHE_ENTITY);
605                     size += 5;
606                     i += 5;
607                     break;
608                 case '\"':
609                     buffer.replace(i, i + 1, QUOTE_ENTITY);
610                     size += 5;
611                     i += 5;
612                     break;
613                 default:
614             }
615         }
616         return buffer.toString();
617     }
618 }