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