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.provider.svn.svnexe.command.changelog;
20  
21  import java.util.ArrayList;
22  import java.util.Date;
23  import java.util.List;
24  import java.util.regex.Matcher;
25  import java.util.regex.Pattern;
26  
27  import org.apache.maven.scm.ChangeFile;
28  import org.apache.maven.scm.ChangeSet;
29  import org.apache.maven.scm.ScmFileStatus;
30  import org.apache.maven.scm.provider.svn.SvnChangeSet;
31  import org.apache.maven.scm.util.AbstractConsumer;
32  
33  /**
34   * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
35   *
36   */
37  public class SvnChangeLogConsumer extends AbstractConsumer {
38      /**
39       * Date formatter for svn timestamp (after a little massaging)
40       */
41      private static final String SVN_TIMESTAMP_PATTERN = "yyyy-MM-dd HH:mm:ss zzzzzzzzz";
42  
43      /**
44       * State machine constant: expecting header
45       */
46      private static final int GET_HEADER = 1;
47  
48      /**
49       * State machine constant: expecting file information
50       */
51      private static final int GET_FILE = 2;
52  
53      /**
54       * State machine constant: expecting comments
55       */
56      private static final int GET_COMMENT = 3;
57  
58      /**
59       * There is always action and affected path; when copying/moving, recognize also original path and revision
60       */
61      private static final Pattern FILE_PATTERN = Pattern.compile("^\\s\\s\\s([A-Z])\\s(.+)$");
62  
63      /**
64       * This matches the 'original file info' part of the complete file line.
65       * Note the use of [:alpha:] instead of literal 'from' - this is meant to allow non-English localizations.
66       */
67      private static final Pattern ORIG_FILE_PATTERN = Pattern.compile("\\([A-Za-z]+ (.+):(\\d+)\\)");
68  
69      /**
70       * The file section ends with a blank line
71       */
72      private static final String FILE_END_TOKEN = "";
73  
74      /**
75       * The comment section ends with a dashed line
76       */
77      private static final String COMMENT_END_TOKEN =
78              "------------------------------------" + "------------------------------------";
79  
80      /**
81       * Current status of the parser
82       */
83      private int status = GET_HEADER;
84  
85      /**
86       * List of change log entries
87       */
88      private final List<ChangeSet> entries = new ArrayList<>();
89  
90      /**
91       * The current log entry being processed by the parser
92       */
93      private SvnChangeSet currentChange;
94  
95      /**
96       * The current revision of the entry being processed by the parser
97       */
98      private String currentRevision;
99  
100     /**
101      * The current comment of the entry being processed by the parser
102      */
103     private StringBuilder currentComment;
104 
105     /**
106      * The regular expression used to match header lines
107      */
108     private static final Pattern HEADER_REG_EXP = Pattern.compile("^(.+) \\| (.+) \\| (.+) \\|.*$");
109 
110     private static final int REVISION_GROUP = 1;
111 
112     private static final int AUTHOR_GROUP = 2;
113 
114     private static final int DATE_GROUP = 3;
115 
116     private static final Pattern REVISION_REG_EXP1 = Pattern.compile("rev (\\d+):");
117 
118     private static final Pattern REVISION_REG_EXP2 = Pattern.compile("r(\\d+)");
119 
120     private static final Pattern DATE_REG_EXP = Pattern.compile("(\\d+-\\d+-\\d+ " + // date 2002-08-24
121             "\\d+:\\d+:\\d+) "
122             + // time 16:01:00
123             "([\\-+])(\\d\\d)(\\d\\d)"); // gmt offset -0400);)
124 
125     private final String userDateFormat;
126 
127     /**
128      * Default constructor.
129      */
130     public SvnChangeLogConsumer(String userDateFormat) {
131         this.userDateFormat = userDateFormat;
132     }
133 
134     public List<ChangeSet> getModifications() {
135         return entries;
136     }
137 
138     // ----------------------------------------------------------------------
139     // StreamConsumer Implementation
140     // ----------------------------------------------------------------------
141 
142     /**
143      * {@inheritDoc}
144      */
145     public void consumeLine(String line) {
146         if (logger.isDebugEnabled()) {
147             logger.debug(line);
148         }
149         switch (status) {
150             case GET_HEADER:
151                 processGetHeader(line);
152                 break;
153             case GET_FILE:
154                 processGetFile(line);
155                 break;
156             case GET_COMMENT:
157                 processGetComment(line);
158                 break;
159             default:
160                 throw new IllegalStateException("Unknown state: " + status);
161         }
162     }
163 
164     // ----------------------------------------------------------------------
165     //
166     // ----------------------------------------------------------------------
167 
168     /**
169      * Process the current input line in the GET_HEADER state.  The
170      * author, date, and the revision of the entry are gathered.  Note,
171      * Subversion does not have per-file revisions, instead, the entire
172      * repository is given a single revision number, which is used for
173      * the revision number of each file.
174      *
175      * @param line A line of text from the svn log output
176      */
177     private void processGetHeader(String line) {
178         Matcher matcher = HEADER_REG_EXP.matcher(line);
179         if (!matcher.matches()) {
180             // The header line is not found. Intentionally do nothing.
181             return;
182         }
183 
184         currentRevision = getRevision(matcher.group(REVISION_GROUP));
185 
186         currentChange = new SvnChangeSet();
187 
188         currentChange.setAuthor(matcher.group(AUTHOR_GROUP));
189 
190         currentChange.setDate(getDate(matcher.group(DATE_GROUP)));
191 
192         currentChange.setRevision(currentRevision);
193 
194         status = GET_FILE;
195     }
196 
197     /**
198      * Gets the svn revision, from the svn log revision output.
199      *
200      * @param revisionOutput
201      * @return the svn revision
202      */
203     private String getRevision(final String revisionOutput) {
204         Matcher matcher;
205         if ((matcher = REVISION_REG_EXP1.matcher(revisionOutput)).matches()) {
206             return matcher.group(1);
207         } else if ((matcher = REVISION_REG_EXP2.matcher(revisionOutput)).matches()) {
208             return matcher.group(1);
209         } else {
210             throw new IllegalOutputException(revisionOutput);
211         }
212     }
213 
214     /**
215      * Process the current input line in the GET_FILE state.  This state
216      * adds each file entry line to the current change log entry.  Note,
217      * the revision number for the entire entry is used for the revision
218      * number of each file.
219      *
220      * @param line A line of text from the svn log output
221      */
222     private void processGetFile(String line) {
223         Matcher matcher = FILE_PATTERN.matcher(line);
224         if (matcher.matches()) {
225             final String fileinfo = matcher.group(2);
226             String name = fileinfo;
227             String originalName = null;
228             String originalRev = null;
229             final int n = fileinfo.indexOf(" (");
230             if (n > 1 && fileinfo.endsWith(")")) {
231                 final String origFileInfo = fileinfo.substring(n);
232                 Matcher matcher2 = ORIG_FILE_PATTERN.matcher(origFileInfo);
233                 if (matcher2.find()) {
234                     // if original file is present, we must extract the affected one from the beginning
235                     name = fileinfo.substring(0, n);
236                     originalName = matcher2.group(1);
237                     originalRev = matcher2.group(2);
238                 }
239             }
240             final String actionStr = matcher.group(1);
241             final ScmFileStatus action;
242             if ("A".equals(actionStr)) {
243                 // TODO: this may even change to MOVED if we later explore whole changeset and find matching DELETED
244                 action = originalRev == null ? ScmFileStatus.ADDED : ScmFileStatus.COPIED;
245             } else if ("D".equals(actionStr)) {
246                 action = ScmFileStatus.DELETED;
247             } else if ("M".equals(actionStr)) {
248                 action = ScmFileStatus.MODIFIED;
249             } else if ("R".equals(actionStr)) {
250                 action = ScmFileStatus.UPDATED; // == REPLACED in svn terms
251             } else {
252                 action = ScmFileStatus.UNKNOWN;
253             }
254             if (logger.isDebugEnabled()) {
255                 logger.debug(actionStr + " : " + name);
256             }
257             final ChangeFile changeFile = new ChangeFile(name, currentRevision);
258             changeFile.setAction(action);
259             changeFile.setOriginalName(originalName);
260             changeFile.setOriginalRevision(originalRev);
261             currentChange.addFile(changeFile);
262 
263             status = GET_FILE;
264         } else if (line.equals(FILE_END_TOKEN)) {
265             // Create a buffer for the collection of the comment now
266             // that we are leaving the GET_FILE state.
267             currentComment = new StringBuilder();
268 
269             status = GET_COMMENT;
270         }
271     }
272 
273     /**
274      * Process the current input line in the GET_COMMENT state.  This
275      * state gathers all of the comments that are part of a log entry.
276      *
277      * @param line a line of text from the svn log output
278      */
279     private void processGetComment(String line) {
280         if (line.equals(COMMENT_END_TOKEN)) {
281             currentChange.setComment(currentComment.toString());
282 
283             entries.add(currentChange);
284 
285             status = GET_HEADER;
286         } else {
287             currentComment.append(line).append('\n');
288         }
289     }
290 
291     /**
292      * Converts the date time stamp from the svn output into a date
293      * object.
294      *
295      * @param dateOutput The date output from an svn log command.
296      * @return A date representing the time stamp of the log entry.
297      */
298     private Date getDate(final String dateOutput) {
299         Matcher matcher = DATE_REG_EXP.matcher(dateOutput);
300         if (!matcher.find()) {
301             throw new IllegalOutputException(dateOutput);
302         }
303 
304         final StringBuilder date = new StringBuilder();
305         date.append(matcher.group(1));
306         date.append(" GMT");
307         date.append(matcher.group(2));
308         date.append(matcher.group(3));
309         date.append(':');
310         date.append(matcher.group(4));
311 
312         return parseDate(date.toString(), userDateFormat, SVN_TIMESTAMP_PATTERN);
313     }
314 }