001package 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
022import org.apache.maven.scm.ChangeFile;
023import org.apache.maven.scm.ChangeSet;
024import org.apache.maven.scm.ScmFileStatus;
025import org.apache.maven.scm.log.ScmLogger;
026import org.apache.maven.scm.provider.svn.SvnChangeSet;
027import org.apache.maven.scm.util.AbstractConsumer;
028import org.apache.regexp.RE;
029
030import java.util.ArrayList;
031import java.util.Date;
032import java.util.List;
033
034/**
035 * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
036 *
037 */
038public 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}