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