View Javadoc

1   /*
2    * LICENSE
3    *
4    * "THE BEER-WARE LICENSE" (Revision 42):
5    * "Sven Strittmatter" <ich@weltraumschaf.de> wrote this file.
6    * As long as you retain this notice you can do whatever you want with
7    * this stuff. If we meet some day, and you think this stuff is worth it,
8    * you can buy me a beer in return.
9    */
10  package org.jenkinsci.plugins.darcs;
11  
12  import java.util.ArrayList;
13  import java.util.HashMap;
14  import java.util.List;
15  import java.util.Map;
16  import java.util.logging.Logger;
17  import org.xml.sax.Attributes;
18  import org.xml.sax.SAXParseException;
19  import org.xml.sax.helpers.DefaultHandler;
20  
21  /**
22   * SAS based change log parser.
23   *
24   * @author Sven Strittmatter <ich@weltraumschaf.de>
25   */
26  class DarcsSaxHandler extends DefaultHandler {
27  
28      /**
29       * Logging facility.
30       */
31      private static final Logger LOGGER = Logger.getLogger(DarcsSaxHandler.class.getName());
32      /**
33       * True attribute value for boolean tag attributes.
34       */
35      private static final String ATTR_TRUE = "True";
36      /**
37       * False attribute value for boolean tag attributes.
38       */
39      private static final String ATTR_FALSE = "False";
40  
41      /**
42       * The tags used in the change log XML.
43       */
44      private enum DarcsChangelogTag {
45  
46          /**
47           * Tag {@literal <changelog>}.
48           */
49          CHANGELOG("changelog"),
50          /**
51           * Tag {@literal <patch>}.
52           */
53          PATCH("patch"),
54          /**
55           * Tag {@literal <name>}.
56           */
57          NAME("name"),
58          /**
59           * Tag {@literal <comment>}.
60           */
61          COMMENT("comment"),
62          /**
63           * Tag {@literal <summary>}.
64           */
65          SUMMARY("summary"),
66          /**
67           * Tag {@literal <modify_file>}.
68           */
69          MODIFY_FILE("modify_file"),
70          /**
71           * Tag {@literal <add_file>}.
72           */
73          ADD_FILE("add_file"),
74          /**
75           * Tag {@literal <remove_file>}.
76           */
77          REMOVE_FILE("remove_file"),
78          /**
79           * Tag {@literal <move>}.
80           */
81          MOVE_FILE("move"),
82          /**
83           * Tag {@literal <added_lines>}.
84           */
85          ADDED_LINES("added_lines"),
86          /**
87           * Tag {@literal <removed_lines>}.
88           */
89          REMOVED_LINES("removed_lines"),
90          /**
91           * Tag {@literal <add_directory>}.
92           */
93          ADD_DIRECTORY("add_directory"),
94          /**
95           * Tag {@literal <remove_directory>}.
96           */
97          REMOVE_DIRECTORY("remove_directory");
98          /**
99           * Lookup of string literal to tag.
100          */
101         private static final Map<String, DarcsChangelogTag> LOOKUP = new HashMap<String, DarcsChangelogTag>();
102 
103         static {
104             for (final DarcsChangelogTag tag : DarcsChangelogTag.values()) {
105                 LOOKUP.put(tag.getTagName().toLowerCase(), tag);
106             }
107         }
108         /**
109          * Literal tag name.
110          */
111         private final String tagName;
112 
113         /**
114          * Dedicated constructor.
115          *
116          * @param tagName the string between the angle brackets.
117          */
118         private DarcsChangelogTag(final String tagName) {
119             this.tagName = tagName;
120         }
121 
122         /**
123          * Get the tag name.
124          *
125          * @return the tag name
126          */
127         public String getTagName() {
128             return tagName;
129         }
130 
131         /**
132          * Returns the tag enum to a literal tag name.
133          *
134          * @param tagName literal tag name, part between the angle brackets
135          * @return may return null, if tag name is unknown
136          */
137         static DarcsChangelogTag forTagName(final String tagName) {
138             if (LOOKUP.containsKey(tagName.toLowerCase())) {
139                 return LOOKUP.get(tagName);
140             }
141 
142             return null;
143         }
144     }
145 
146     /**
147      * Attributes the {@literal <patch>} has.
148      */
149     private enum DarcsPatchTagAttribute {
150 
151         /**
152          * Author attribute.
153          */
154         AUTHOR("author"),
155         /**
156          * date attribute.
157          */
158         DATE("date"),
159         /**
160          * Local date attribute.
161          */
162         LOCAL_DATE("local_date"),
163         /**
164          * Hash attribute.
165          */
166         HASH("hash"),
167         /**
168          * Inverted attribute.
169          */
170         INVERTED("inverted");
171         /**
172          * Name of the attribute.
173          */
174         private final String name;
175 
176         /**
177          * Dedicated constructor.
178          *
179          * @param name of the attribute
180          */
181         private DarcsPatchTagAttribute(String name) {
182             this.name = name;
183         }
184 
185         /**
186          * Get the attribute name.
187          *
188          * @return lower cased attribute name
189          */
190         public String getName() {
191             return name;
192         }
193     }
194 
195     /**
196      * Attributes the {@literal <move>} has.
197      */
198     private enum DarcsMoveTagAttribute {
199         /** From attribute. */
200         FROM("from"),
201         /** To attribute. */
202         TO("to");
203         /**
204          * Name of the attribute.
205          */
206         private final String name;
207 
208         /**
209          * Dedicated constructor.
210          *
211          * @param name of the attribute
212          */
213         private DarcsMoveTagAttribute(String name) {
214             this.name = name;
215         }
216 
217         /**
218          * Get the attribute name.
219          *
220          * @return lower cased attribute name
221          */
222         public String getName() {
223             return name;
224         }
225     }
226     /**
227      * The current parsed tag.
228      */
229     private DarcsChangelogTag currentTag;
230     /**
231      * Current processed change set.
232      */
233     private DarcsChangeSet currentChangeSet;
234     /**
235      * Signals that parsing has ended.
236      */
237     private boolean ready;
238     /**
239      * Change sets collected during the parse process.
240      */
241     private final List<DarcsChangeSet> changeSets = new ArrayList<DarcsChangeSet>();
242     /**
243      * Buffers scanned literals.
244      */
245     private StringBuilder literalBuffer = new StringBuilder();
246 
247     /**
248      * Dedicated constructor.
249      */
250     public DarcsSaxHandler() {
251         super();
252     }
253 
254     /**
255      * Returns if the parsing of the XML has ended.
256      *
257      * @return {@code true} if end of document reached, else {@code false}
258      */
259     public boolean isReady() {
260         return ready;
261     }
262 
263     /**
264      * Get the list of parsed change sets.
265      *
266      * @return may be an empty list, but never {@code null}
267      */
268     public List<DarcsChangeSet> getChangeSets() {
269         return changeSets;
270     }
271 
272     @Override
273     public void endDocument() {
274         ready = true;
275     }
276 
277     /**
278      * Recognizes the current scanned tag.
279      *
280      * Logs a warning if unrecognizable tag occurred and set {@link #currentTag} to {@value null}.
281      *
282      * @param tagName scanned tag name
283      */
284     private void recognizeTag(final String tagName) {
285         final DarcsChangelogTag tag = DarcsChangelogTag.forTagName(tagName);
286 
287         if (null == tag) {
288             LOGGER.warning(String.format("Unrecognized tag <%s>!", tagName));
289         } else {
290             currentTag = tag;
291         }
292     }
293 
294     @Override
295     public void startElement(final String uri, final String name, final String qName, final Attributes atts) {
296         if (DarcsChangelogTag.MODIFY_FILE == currentTag) {
297             currentChangeSet.getModifiedPaths().add(literalBuffer.toString());
298         }
299 
300         recognizeTag(qName);
301 
302         if (DarcsChangelogTag.PATCH == currentTag) {
303             currentChangeSet = new DarcsChangeSet();
304             currentChangeSet.setAuthor(atts.getValue(DarcsPatchTagAttribute.AUTHOR.getName()));
305             currentChangeSet.setDate(atts.getValue(DarcsPatchTagAttribute.DATE.getName()));
306             currentChangeSet.setLocalDate(atts.getValue(DarcsPatchTagAttribute.LOCAL_DATE.getName()));
307             currentChangeSet.setHash(atts.getValue(DarcsPatchTagAttribute.HASH.getName()));
308 
309             if (ATTR_TRUE.equalsIgnoreCase(atts.getValue(DarcsPatchTagAttribute.INVERTED.getName()))) {
310                 currentChangeSet.setInverted(true);
311             } else if (ATTR_FALSE.equalsIgnoreCase(atts.getValue(DarcsPatchTagAttribute.INVERTED.getName()))) {
312                 currentChangeSet.setInverted(false);
313             }
314         } else if (DarcsChangelogTag.MOVE_FILE == currentTag) {
315             currentChangeSet.getDeletedPaths().add(atts.getValue(DarcsMoveTagAttribute.FROM.getName()));
316             currentChangeSet.getAddedPaths().add(atts.getValue(DarcsMoveTagAttribute.TO.getName()));
317         }
318 
319         literalBuffer = new StringBuilder();
320     }
321 
322     @Override
323     public void endElement(final String uri, final String name, final String qName) {
324         recognizeTag(qName);
325 
326         switch (currentTag) {
327             case PATCH:
328                 changeSets.add(currentChangeSet);
329                 break;
330             case NAME:
331                 currentChangeSet.setName(literalBuffer.toString());
332                 break;
333             case COMMENT:
334                 final String comment = stripIgnoreThisFromComment(literalBuffer.toString());
335                 currentChangeSet.setComment(comment);
336                 break;
337             case ADD_FILE:
338             case ADD_DIRECTORY:
339                 currentChangeSet.getAddedPaths().add(literalBuffer.toString());
340                 break;
341             case REMOVE_FILE:
342             case REMOVE_DIRECTORY:
343                 currentChangeSet.getDeletedPaths().add(literalBuffer.toString());
344                 break;
345             default:
346                 LOGGER.info(String.format("Ignored tag <%s>!", currentTag));
347         }
348 
349         currentTag = null;
350     }
351 
352     /**
353      * Strips out strings like "Ignore-this: 606c40ef0d257da9b7a916e7f1c594aa".
354      *
355      * It is assumed that after the hash a single line break occurred.
356      *
357      * @param comment comment message
358      * @return cleaned comment message
359      */
360     static String stripIgnoreThisFromComment(final String comment) {
361         if (comment.startsWith("Ignore-this:")) {
362             final int end = comment.indexOf("\n");
363 
364             if (-1 == end) {
365                 return "";
366             }
367 
368             return comment.substring(end + 1);
369         }
370 
371         return comment;
372     }
373 
374     /**
375      * Determine whether a character is a whitespace character or not.
376      *
377      * @param c character to check
378      * @return {@code true} if passed in char is one of \n, \r, \t, ' '; else {@code false}
379      */
380     private boolean isWhiteSpace(final char c) {
381         switch (c) {
382             case '\n':
383             case '\r':
384             case '\t':
385             case ' ':
386                 return true;
387             default:
388                 return false;
389         }
390     }
391 
392     /**
393      * Return whether to skip white spaces.
394      *
395      * White spaces are not skipped if parsing the text of name and comment tags.
396      *
397      * @return {@code false} if current tag is {@value DarcsChangelogTag#NAME} or {@value DarcsChangelogTag#COMMENT};
398      * else {@code false}
399      */
400     private boolean skipWhiteSpace() {
401         return DarcsChangelogTag.NAME != currentTag && DarcsChangelogTag.COMMENT != currentTag;
402     }
403 
404     @Override
405     public void characters(final char[] ch, final int start, final int length) {
406         for (int i = start; i < start + length; i++) {
407             if (isWhiteSpace(ch[i]) && skipWhiteSpace()) {
408                 continue;
409             }
410 
411             literalBuffer.append(ch[i]);
412         }
413     }
414 
415     @Override
416     public void error(final SAXParseException saxpe) {
417         LOGGER.warning(saxpe.toString());
418     }
419 
420     @Override
421     public void fatalError(final SAXParseException saxpe) {
422         LOGGER.warning(saxpe.toString());
423     }
424 
425     @Override
426     public void warning(final SAXParseException saxpe) {
427         LOGGER.warning(saxpe.toString());
428     }
429 }