001    package org.apache.maven.scm.provider.svn.svnexe.command.changelog;
002    
003    /*
004     * Licensed to the Apache Software Foundation (ASF) under one
005     * or more contributor license agreements.  See the NOTICE file
006     * distributed with this work for additional information
007     * regarding copyright ownership.  The ASF licenses this file
008     * to you under the Apache License, Version 2.0 (the
009     * "License"); you may not use this file except in compliance
010     * with the License.  You may obtain a copy of the License at
011     *
012     * http://www.apache.org/licenses/LICENSE-2.0
013     *
014     * Unless required by applicable law or agreed to in writing,
015     * software distributed under the License is distributed on an
016     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017     * KIND, either express or implied.  See the License for the
018     * specific language governing permissions and limitations
019     * under the License.
020     */
021    
022    import org.apache.maven.scm.ChangeFile;
023    import org.apache.maven.scm.ChangeSet;
024    import org.apache.maven.scm.ScmFileStatus;
025    import org.apache.maven.scm.log.ScmLogger;
026    import org.apache.maven.scm.provider.svn.SvnChangeSet;
027    import org.apache.maven.scm.util.AbstractConsumer;
028    import org.apache.regexp.RE;
029    
030    import java.util.ArrayList;
031    import java.util.Date;
032    import java.util.List;
033    
034    /**
035     * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
036     * @version $Id: SvnChangeLogConsumer.java 1306862 2012-03-29 13:42:39Z olamy $
037     */
038    public class SvnChangeLogConsumer
039        extends AbstractConsumer
040    {
041        /**
042         * Date formatter for svn timestamp (after a little massaging)
043         */
044        private static final String SVN_TIMESTAMP_PATTERN = "yyyy-MM-dd HH:mm:ss zzzzzzzzz";
045    
046        /**
047         * State machine constant: expecting header
048         */
049        private static final int GET_HEADER = 1;
050    
051        /**
052         * State machine constant: expecting file information
053         */
054        private static final int GET_FILE = 2;
055    
056        /**
057         * State machine constant: expecting comments
058         */
059        private static final int GET_COMMENT = 3;
060    
061        /**
062         * There is always action and affected path; when copying/moving, recognize also original path and revision
063         */
064        private static final RE FILE_PATTERN = new RE( "^\\s\\s\\s([:upper:])\\s(.+)$" );
065    
066        /**
067         * This matches the 'original file info' part of the complete file line.
068         * Note the use of [:alpha:] instead of literal 'from' - this is meant to allow non-English localizations.
069         */
070        private static final RE ORIG_FILE_PATTERN = new RE( "\\([:alpha:]+ (.+):(\\d+)\\)" );
071    
072        /**
073         * The file section ends with a blank line
074         */
075        private static final String FILE_END_TOKEN = "";
076    
077        /**
078         * The comment section ends with a dashed line
079         */
080        private static final String COMMENT_END_TOKEN =
081            "------------------------------------" + "------------------------------------";
082    
083        /**
084         * Current status of the parser
085         */
086        private int status = GET_HEADER;
087    
088        /**
089         * List of change log entries
090         */
091        private List<ChangeSet> entries = new ArrayList<ChangeSet>();
092    
093        /**
094         * The current log entry being processed by the parser
095         */
096        private SvnChangeSet currentChange;
097    
098        /**
099         * 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 RE HEADER_REG_EXP = new RE( "^(.+) \\| (.+) \\| (.+) \\|.*$" );
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 RE REVISION_REG_EXP1 = new RE( "rev (\\d+):" );
120    
121        private static final RE REVISION_REG_EXP2 = new RE( "r(\\d+)" );
122    
123        private static final RE DATE_REG_EXP = new RE( "(\\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( ScmLogger logger, String userDateFormat )
133        {
134            super( logger );
135    
136            this.userDateFormat = userDateFormat;
137        }
138    
139        public List<ChangeSet> getModifications()
140        {
141            return entries;
142        }
143    
144        // ----------------------------------------------------------------------
145        // StreamConsumer Implementation
146        // ----------------------------------------------------------------------
147    
148        /**
149         * {@inheritDoc}
150         */
151        public void consumeLine( String line )
152        {
153            if ( getLogger().isDebugEnabled() )
154            {
155                getLogger().debug( line );
156            }
157            switch ( status )
158            {
159                case GET_HEADER:
160                    processGetHeader( line );
161                    break;
162                case GET_FILE:
163                    processGetFile( line );
164                    break;
165                case GET_COMMENT:
166                    processGetComment( line );
167                    break;
168                default:
169                    throw new IllegalStateException( "Unknown state: " + status );
170            }
171        }
172    
173        // ----------------------------------------------------------------------
174        //
175        // ----------------------------------------------------------------------
176    
177        /**
178         * Process the current input line in the GET_HEADER state.  The
179         * author, date, and the revision of the entry are gathered.  Note,
180         * Subversion does not have per-file revisions, instead, the entire
181         * repository is given a single revision number, which is used for
182         * the revision number of each file.
183         *
184         * @param line A line of text from the svn log output
185         */
186        private void processGetHeader( String line )
187        {
188            if ( !HEADER_REG_EXP.match( line ) )
189            {
190                // The header line is not found. Intentionally do nothing.
191                return;
192            }
193    
194            currentRevision = getRevision( HEADER_REG_EXP.getParen( REVISION_GROUP ) );
195    
196            currentChange = new SvnChangeSet();
197    
198            currentChange.setAuthor( HEADER_REG_EXP.getParen( AUTHOR_GROUP ) );
199    
200            currentChange.setDate( getDate( HEADER_REG_EXP.getParen( DATE_GROUP ) ) );
201    
202            currentChange.setRevision( currentRevision );
203    
204            status = GET_FILE;
205        }
206    
207        /**
208         * Gets the svn revision, from the svn log revision output.
209         *
210         * @param revisionOutput
211         * @return the svn revision
212         */
213        private String getRevision( final String revisionOutput )
214        {
215            if ( REVISION_REG_EXP1.match( revisionOutput ) )
216            {
217                return REVISION_REG_EXP1.getParen( 1 );
218            }
219            else if ( REVISION_REG_EXP2.match( revisionOutput ) )
220            {
221                return REVISION_REG_EXP2.getParen( 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            if ( FILE_PATTERN.match( line ) )
240            {
241                final String fileinfo = FILE_PATTERN.getParen( 2 );
242                String name = fileinfo;
243                String originalName = null;
244                String originalRev = null;
245                final int n = fileinfo.indexOf( " (" );
246                if ( n > 1 && fileinfo.endsWith( ")" ) )
247                {
248                    final String origFileInfo = fileinfo.substring( n );
249                    if ( ORIG_FILE_PATTERN.match( origFileInfo ) )
250                    {
251                        // if original file is present, we must extract the affected one from the beginning
252                        name = fileinfo.substring( 0, n );
253                        originalName = ORIG_FILE_PATTERN.getParen( 1 );
254                        originalRev = ORIG_FILE_PATTERN.getParen( 2 );
255                    }
256                }
257                final String actionStr = FILE_PATTERN.getParen( 1 );
258                final ScmFileStatus action;
259                if ( "A".equals( actionStr ) )
260                {
261                    //TODO: this may even change to MOVED if we later explore whole changeset and find matching DELETED
262                    action = originalRev == null ? ScmFileStatus.ADDED : ScmFileStatus.COPIED;
263                }
264                else if ( "D".equals( actionStr ) )
265                {
266                    action = ScmFileStatus.DELETED;
267                }
268                else if ( "M".equals( actionStr ) )
269                {
270                    action = ScmFileStatus.MODIFIED;
271                }
272                else if ( "R".equals( actionStr ) )
273                {
274                    action = ScmFileStatus.UPDATED; //== REPLACED in svn terms
275                }
276                else
277                {
278                    action = ScmFileStatus.UNKNOWN;
279                }
280                System.out.println( actionStr + " : " + name );
281                final ChangeFile changeFile = new ChangeFile( name, currentRevision );
282                changeFile.setAction( action );
283                changeFile.setOriginalName( originalName );
284                changeFile.setOriginalRevision( originalRev );
285                currentChange.addFile( changeFile );
286    
287                status = GET_FILE;
288            }
289            else if ( line.equals( FILE_END_TOKEN ) )
290            {
291                // Create a buffer for the collection of the comment now
292                // that we are leaving the GET_FILE state.
293                currentComment = new StringBuilder();
294    
295                status = GET_COMMENT;
296            }
297        }
298    
299        /**
300         * Process the current input line in the GET_COMMENT state.  This
301         * state gathers all of the comments that are part of a log entry.
302         *
303         * @param line a line of text from the svn log output
304         */
305        private void processGetComment( String line )
306        {
307            if ( line.equals( COMMENT_END_TOKEN ) )
308            {
309                currentChange.setComment( currentComment.toString() );
310    
311                entries.add( currentChange );
312    
313                status = GET_HEADER;
314            }
315            else
316            {
317                currentComment.append( line ).append( '\n' );
318            }
319        }
320    
321        /**
322         * Converts the date time stamp from the svn output into a date
323         * object.
324         *
325         * @param dateOutput The date output from an svn log command.
326         * @return A date representing the time stamp of the log entry.
327         */
328        private Date getDate( final String dateOutput )
329        {
330            if ( !DATE_REG_EXP.match( dateOutput ) )
331            {
332                throw new IllegalOutputException( dateOutput );
333            }
334    
335            final StringBuilder date = new StringBuilder();
336            date.append( DATE_REG_EXP.getParen( 1 ) );
337            date.append( " GMT" );
338            date.append( DATE_REG_EXP.getParen( 2 ) );
339            date.append( DATE_REG_EXP.getParen( 3 ) );
340            date.append( ':' );
341            date.append( DATE_REG_EXP.getParen( 4 ) );
342    
343            return parseDate( date.toString(), userDateFormat, SVN_TIMESTAMP_PATTERN );
344        }
345    }