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;
028
029import java.util.ArrayList;
030import java.util.Date;
031import java.util.List;
032import java.util.regex.Matcher;
033import java.util.regex.Pattern;
034
035/**
036 * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
037 *
038 */
039public class SvnChangeLogConsumer
040    extends AbstractConsumer
041{
042    /**
043     * Date formatter for svn timestamp (after a little massaging)
044     */
045    private static final String SVN_TIMESTAMP_PATTERN = "yyyy-MM-dd HH:mm:ss zzzzzzzzz";
046
047    /**
048     * State machine constant: expecting header
049     */
050    private static final int GET_HEADER = 1;
051
052    /**
053     * State machine constant: expecting file information
054     */
055    private static final int GET_FILE = 2;
056
057    /**
058     * State machine constant: expecting comments
059     */
060    private static final int GET_COMMENT = 3;
061
062    /**
063     * There is always action and affected path; when copying/moving, recognize also original path and revision
064     */
065    private static final Pattern FILE_PATTERN = Pattern.compile("^\\s\\s\\s([A-Z])\\s(.+)$");
066
067    /**
068     * This matches the 'original file info' part of the complete file line.
069     * Note the use of [:alpha:] instead of literal 'from' - this is meant to allow non-English localizations.
070     */
071    private static final Pattern ORIG_FILE_PATTERN = Pattern.compile( "\\([A-Za-z]+ (.+):(\\d+)\\)" );
072
073    /**
074     * The file section ends with a blank line
075     */
076    private static final String FILE_END_TOKEN = "";
077
078    /**
079     * The comment section ends with a dashed line
080     */
081    private static final String COMMENT_END_TOKEN =
082        "------------------------------------" + "------------------------------------";
083
084    /**
085     * Current status of the parser
086     */
087    private int status = GET_HEADER;
088
089    /**
090     * List of change log entries
091     */
092    private List<ChangeSet> entries = new ArrayList<ChangeSet>();
093
094    /**
095     * The current log entry being processed by the parser
096     */
097    private SvnChangeSet currentChange;
098
099    /**
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}