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