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  
35  /**
36   * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
37   */
38  public class ChangeSet implements Serializable {
39      private static final long serialVersionUID = 7097705862222539801L;
40  
41      /**
42       * Escaped <code>&lt;</code> entity.
43       */
44      public static final String LESS_THAN_ENTITY = "&lt;";
45  
46      /**
47       * Escaped <code>&gt;</code> entity.
48       */
49      public static final String GREATER_THAN_ENTITY = "&gt;";
50  
51      /**
52       * Escaped <code>&amp;</code> entity.
53       */
54      public static final String AMPERSAND_ENTITY = "&amp;";
55  
56      /**
57       * Escaped <code>'</code> entity.
58       */
59      public static final String APOSTROPHE_ENTITY = "&apos;";
60  
61      /**
62       * Escaped <code>"</code> entity.
63       */
64      public static final String QUOTE_ENTITY = "&quot;";
65  
66      private static final String DATE_PATTERN = "yyyy-MM-dd";
67  
68      /**
69       * Formatter used by the getDateFormatted method.
70       */
71      private static final ThreadSafeDateFormat DATE_FORMAT = new ThreadSafeDateFormat(DATE_PATTERN);
72  
73      private static final String TIME_PATTERN = "HH:mm:ss";
74  
75      /**
76       * Formatter used by the getTimeFormatted method.
77       */
78      private static final ThreadSafeDateFormat TIME_FORMAT = new ThreadSafeDateFormat(TIME_PATTERN);
79  
80      /**
81       * Formatter used to parse date/timestamp.
82       */
83      private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_1 = new ThreadSafeDateFormat("yyyy/MM/dd HH:mm:ss");
84  
85      private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_2 = new ThreadSafeDateFormat("yyyy-MM-dd HH:mm:ss");
86  
87      private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_3 = new ThreadSafeDateFormat("yyyy/MM/dd HH:mm:ss z");
88  
89      private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_4 = new ThreadSafeDateFormat("yyyy-MM-dd HH:mm:ss z");
90  
91      /**
92       * Date the changes were committed.
93       */
94      private Date date;
95  
96      /**
97       * User who made changes.
98       */
99      private String author;
100 
101     /**
102      * Comment provided at commit time.
103      */
104     private String comment = "";
105 
106     /**
107      * List of ChangeFile.
108      */
109     private List<ChangeFile> files;
110 
111     /**
112      * List of tags.
113      */
114     private List<String> tags;
115 
116     /**
117      * The SCM revision id for this changeset.
118      *
119      * @since 1.3
120      */
121     private String revision;
122 
123     /**
124      * Revision from which this one originates.
125      *
126      * @since 1.7
127      */
128     private String parentRevision;
129 
130     /**
131      * Revisions that were merged into this one.
132      *
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, List<ChangeFile> files) {
145         this(null, comment, author, files);
146 
147         setDate(strDate, userDatePattern);
148     }
149 
150     /**
151      * @param date    date the changes were committed
152      * @param comment comment provided at commit time
153      * @param author  user who made changes
154      * @param files   the ChangeFile list
155      */
156     public ChangeSet(Date date, String comment, String author, List<ChangeFile> files) {
157         setDate(date);
158 
159         setAuthor(author);
160 
161         setComment(comment);
162 
163         this.files = files;
164     }
165 
166     /**
167      * Constructor used when attributes aren't available until later.
168      */
169     public ChangeSet() {
170         // no op
171     }
172 
173     /**
174      * Getter for ChangeFile list.
175      *
176      * @return list of ChangeFile
177      */
178     public List<ChangeFile> getFiles() {
179         if (files == null) {
180             return new ArrayList<>();
181         }
182         return files;
183     }
184 
185     /**
186      * Setter for ChangeFile list.
187      *
188      * @param files list of ChangeFiles
189      */
190     public void setFiles(List<ChangeFile> files) {
191         this.files = files;
192     }
193 
194     public void addFile(ChangeFile file) {
195         if (files == null) {
196             files = new ArrayList<>();
197         }
198 
199         files.add(file);
200     }
201 
202     /**
203      * @param filename TODO
204      * @param repository NOT USED
205      * @return TODO
206      * @deprecated use method {@link #containsFilename(String)}
207      */
208     public boolean containsFilename(String filename, ScmProviderRepository repository) {
209         return containsFilename(filename);
210     }
211 
212     public boolean containsFilename(String filename) {
213         if (files != null) {
214             for (ChangeFile file : files) {
215                 String f1 = FilenameUtils.normalizeFilename(file.getName());
216                 String f2 = FilenameUtils.normalizeFilename(filename);
217                 if (f1.indexOf(f2) >= 0) {
218                     return true;
219                 }
220             }
221         }
222 
223         return false;
224     }
225 
226     /**
227      * Getter for property author.
228      *
229      * @return value of property author
230      */
231     public String getAuthor() {
232         return author;
233     }
234 
235     /**
236      * Setter for property author.
237      *
238      * @param author new value of property author
239      */
240     public void setAuthor(String author) {
241         this.author = author;
242     }
243 
244     /**
245      * Getter for property comment.
246      *
247      * @return value of property comment
248      */
249     public String getComment() {
250         return comment;
251     }
252 
253     /**
254      * Setter for property comment.
255      *
256      * @param comment new value of property comment
257      */
258     public void setComment(String comment) {
259         this.comment = comment;
260     }
261 
262     /**
263      * Getter for property date.
264      *
265      * @return value of property date
266      */
267     public Date getDate() {
268         if (date != null) {
269             return (Date) date.clone();
270         }
271 
272         return null;
273     }
274 
275     /**
276      * Setter for property date.
277      *
278      * @param date new value of property date
279      */
280     public void setDate(Date date) {
281         if (date != null) {
282             this.date = new Date(date.getTime());
283         }
284     }
285 
286     /**
287      * Setter for property date that takes a string and parses it.
288      *
289      * @param date a string in yyyy/MM/dd HH:mm:ss format
290      */
291     public void setDate(String date) {
292         setDate(date, null);
293     }
294 
295     /**
296      * Setter for property date that takes a string and parses it.
297      *
298      * @param date            a string in yyyy/MM/dd HH:mm:ss format
299      * @param userDatePattern pattern of date
300      */
301     public void setDate(String date, String userDatePattern) {
302         try {
303             if (!(userDatePattern == null || userDatePattern.isEmpty())) {
304                 SimpleDateFormat format = new SimpleDateFormat(userDatePattern);
305 
306                 this.date = format.parse(date);
307             } else {
308                 this.date = TIMESTAMP_FORMAT_3.parse(date);
309             }
310         } catch (ParseException e) {
311             if (!(userDatePattern == null || userDatePattern.isEmpty())) {
312                 try {
313                     this.date = TIMESTAMP_FORMAT_3.parse(date);
314                 } catch (ParseException pe) {
315                     try {
316                         this.date = TIMESTAMP_FORMAT_4.parse(date);
317                     } catch (ParseException pe1) {
318                         try {
319                             this.date = TIMESTAMP_FORMAT_1.parse(date);
320                         } catch (ParseException pe2) {
321                             try {
322                                 this.date = TIMESTAMP_FORMAT_2.parse(date);
323                             } catch (ParseException pe3) {
324                                 throw new IllegalArgumentException("Unable to parse date: " + date);
325                             }
326                         }
327                     }
328                 }
329             } else {
330                 try {
331                     this.date = TIMESTAMP_FORMAT_4.parse(date);
332                 } catch (ParseException pe1) {
333                     try {
334                         this.date = TIMESTAMP_FORMAT_1.parse(date);
335                     } catch (ParseException pe2) {
336                         try {
337                             this.date = TIMESTAMP_FORMAT_2.parse(date);
338                         } catch (ParseException pe3) {
339                             throw new IllegalArgumentException("Unable to parse date: " + date);
340                         }
341                     }
342                 }
343             }
344         }
345     }
346 
347     /**
348      * @return date in yyyy-mm-dd format
349      */
350     public String getDateFormatted() {
351         return DATE_FORMAT.format(getDate());
352     }
353 
354     /**
355      * @return time in HH:mm:ss format
356      */
357     public String getTimeFormatted() {
358         return TIME_FORMAT.format(getDate());
359     }
360 
361     /**
362      * Getter for property tags.
363      *
364      * @return value of property author
365      */
366     public List<String> getTags() {
367         if (tags == null) {
368             return new ArrayList<>();
369         }
370         return tags;
371     }
372 
373     /**
374      * Setter for property tags.
375      *
376      * @param tags new value of property tags. This replaces the existing list (if any).
377      */
378     public void setTags(List<String> tags) {
379         this.tags = tags;
380     }
381 
382     /**
383      * Setter for property tags.
384      *
385      * @param tag new tag to add to the list of tags
386      */
387     public void addTag(String tag) {
388         if (tag == null) {
389             return;
390         }
391         tag = tag.trim();
392         if (tag.isEmpty()) {
393             return;
394         }
395         if (tags == null) {
396             tags = new ArrayList<>();
397         }
398         tags.add(tag);
399     }
400 
401     /**
402      * @return TODO
403      * @since 1.3
404      */
405     public String getRevision() {
406         return revision;
407     }
408 
409     /**
410      * @param revision TODO
411      * @since 1.3
412      */
413     public void setRevision(String revision) {
414         this.revision = revision;
415     }
416 
417     public String getParentRevision() {
418         return parentRevision;
419     }
420 
421     public void setParentRevision(String parentRevision) {
422         this.parentRevision = parentRevision;
423     }
424 
425     public void addMergedRevision(String mergedRevision) {
426         if (mergedRevisions == null) {
427             mergedRevisions = new LinkedHashSet<>();
428         }
429         mergedRevisions.add(mergedRevision);
430     }
431 
432     public Set<String> getMergedRevisions() {
433         return mergedRevisions == null ? Collections.emptySet() : mergedRevisions;
434     }
435 
436     public void setMergedRevisions(Set<String> mergedRevisions) {
437         this.mergedRevisions = mergedRevisions;
438     }
439 
440     /**
441      * {@inheritDoc}
442      */
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     /**
535      * {@inheritDoc}
536      */
537     public boolean equals(Object obj) {
538         if (obj instanceof ChangeSet) {
539             ChangeSet changeSet = (ChangeSet) obj;
540 
541             if (toString().equals(changeSet.toString())) {
542                 return true;
543             }
544         }
545 
546         return false;
547     }
548 
549     /**
550      * {@inheritDoc}
551      */
552     public int hashCode() {
553         final int prime = 31;
554         int result = 1;
555         result = prime * result + ((author == null) ? 0 : author.hashCode());
556         result = prime * result + ((comment == null) ? 0 : comment.hashCode());
557         result = prime * result + ((date == null) ? 0 : date.hashCode());
558         result = prime * result + ((parentRevision == null) ? 0 : parentRevision.hashCode());
559         result = prime * result + ((mergedRevisions == null) ? 0 : mergedRevisions.hashCode());
560         result = prime * result + ((files == null) ? 0 : files.hashCode());
561         return result;
562     }
563 
564     /**
565      * Remove a <code>]]></code> from comments (replace it with <code>] ] ></code>).
566      *
567      * @param message the message to modify
568      * @return a clean string
569      */
570     private String removeCDataEnd(String message) {
571         // check for invalid sequence ]]>
572         int endCdata;
573         while (message != null && (endCdata = message.indexOf("]]>")) > -1) {
574             message = message.substring(0, endCdata) + "] ] >" + message.substring(endCdata + 3, message.length());
575         }
576         return message;
577     }
578 
579     /**
580      * <p>Escape the <code>toString</code> of the given object.
581      * For use in an attribute value.</p>
582      * <p>
583      * swiped from jakarta-commons/betwixt -- XMLUtils.java.
584      *
585      * @param value escape <code>value.toString()</code>
586      * @return text with characters restricted (for use in attributes) escaped
587      */
588     public static String escapeValue(Object value) {
589         StringBuilder buffer = new StringBuilder(value.toString());
590         for (int i = 0, size = buffer.length(); i < size; i++) {
591             switch (buffer.charAt(i)) {
592                 case '<':
593                     buffer.replace(i, i + 1, LESS_THAN_ENTITY);
594                     size += 3;
595                     i += 3;
596                     break;
597                 case '>':
598                     buffer.replace(i, i + 1, GREATER_THAN_ENTITY);
599                     size += 3;
600                     i += 3;
601                     break;
602                 case '&':
603                     buffer.replace(i, i + 1, AMPERSAND_ENTITY);
604                     size += 4;
605                     i += 4;
606                     break;
607                 case '\'':
608                     buffer.replace(i, i + 1, APOSTROPHE_ENTITY);
609                     size += 5;
610                     i += 5;
611                     break;
612                 case '\"':
613                     buffer.replace(i, i + 1, QUOTE_ENTITY);
614                     size += 5;
615                     i += 5;
616                     break;
617                 default:
618             }
619         }
620         return buffer.toString();
621     }
622 }