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