001package org.apache.maven.scm.provider.perforce.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 java.util.ArrayList;
023import java.util.List;
024import java.util.regex.Matcher;
025import java.util.regex.Pattern;
026
027import org.apache.maven.scm.ChangeFile;
028import org.apache.maven.scm.ChangeSet;
029import org.apache.maven.scm.ScmException;
030import org.apache.maven.scm.log.ScmLogger;
031import org.apache.maven.scm.util.AbstractConsumer;
032
033/**
034 * Parse the tagged output from "p4 describe -s [change] [change] [...]".
035 *
036 * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
037 * @author Olivier Lamy
038 *
039 */
040public class PerforceDescribeConsumer
041    extends AbstractConsumer
042{
043    
044    private List<ChangeSet> entries = new ArrayList<ChangeSet>();
045
046    /**
047     * State machine constant: expecting revision
048     */
049    private static final int GET_REVISION = 1;
050
051    /**
052     * State machine constant: eat the first blank line
053     */
054    private static final int GET_COMMENT_BEGIN = 2;
055
056    /**
057     * State machine constant: expecting comments
058     */
059    private static final int GET_COMMENT = 3;
060
061    /**
062     * State machine constant: expecting "Affected files"
063     */
064    private static final int GET_AFFECTED_FILES = 4;
065
066    /**
067     * State machine constant: expecting blank line
068     */
069    private static final int GET_FILES_BEGIN = 5;
070
071    /**
072     * State machine constant: expecting files
073     */
074    private static final int GET_FILE = 6;
075
076    /**
077     * Current status of the parser
078     */
079    private int status = GET_REVISION;
080
081    /**
082     * The current log entry being processed by the parser
083     */
084    @SuppressWarnings( "unused" )
085    private String currentRevision;
086
087    /**
088     * The current log entry being processed by the parser
089     */
090    private ChangeSet currentChange;
091
092    /**
093     * the current file being processed by the parser
094     */
095    private String currentFile;
096
097    /**
098     * The location of files within the Perforce depot that we are processing
099     * e.g. //depot/projects/foo/bar
100     */
101    private String repoPath;
102
103    private String userDatePattern;
104
105    /**
106     * The regular expression used to match header lines
107     */
108    private static final Pattern REVISION_PATTERN = Pattern.compile( "^Change (\\d+) " + // changelist number
109        "by (.*)@[^ ]+ " + // author
110        "on (.*)" ); // date
111    /**
112     * The comment section ends with a blank line
113     */
114    private static final String COMMENT_DELIMITER = "";
115    /**
116     * The changelist ends with a blank line
117     */
118    private static final String CHANGELIST_DELIMITER = "";
119
120    /**
121     * The regular expression used to match file paths
122     */
123    private static final Pattern FILE_PATTERN = Pattern.compile( "^\\.\\.\\. (.*)#(\\d+) " );
124
125    public PerforceDescribeConsumer( String repoPath, String userDatePattern, ScmLogger logger )
126    {
127        super( logger );
128
129        this.repoPath = repoPath;
130        this.userDatePattern = userDatePattern;
131    }
132
133    // ----------------------------------------------------------------------
134    //
135    // ----------------------------------------------------------------------
136
137    public List<ChangeSet> getModifications() throws ScmException
138    {
139        return entries;
140    }
141
142    // ----------------------------------------------------------------------
143    // StreamConsumer Implementation
144    // ----------------------------------------------------------------------
145
146    /** {@inheritDoc} */
147    public void consumeLine( String line )
148    {
149        switch ( status )
150        {
151            case GET_REVISION:
152                processGetRevision( line );
153                break;
154            case GET_COMMENT_BEGIN:
155                status = GET_COMMENT;
156                break;
157            case GET_COMMENT:
158                processGetComment( line );
159                break;
160            case GET_AFFECTED_FILES:
161                processGetAffectedFiles( line );
162                break;
163            case GET_FILES_BEGIN:
164                status = GET_FILE;
165                break;
166            case GET_FILE:
167                processGetFile( line );
168                break;
169            default:
170                throw new IllegalStateException( "Unknown state: " + status );
171        }
172    }
173
174    // ----------------------------------------------------------------------
175    //
176    // ----------------------------------------------------------------------
177
178    /**
179     * Add a change log entry to the list (if it's not already there)
180     * with the given file.
181     *
182     * @param entry a {@link ChangeSet} to be added to the list if another
183     *              with the same key (p4 change number) doesn't exist already.
184     * @param file  a {@link ChangeFile} to be added to the entry
185     */
186    private void addEntry( ChangeSet entry, ChangeFile file )
187    {
188        entry.addFile( file );
189    }
190
191    /**
192     * Each file matches the fileRegexp.
193     *
194     * @param line A line of text from the Perforce log output
195     */
196    private void processGetFile( String line )
197    {
198        if ( line.equals( CHANGELIST_DELIMITER ) ) {
199            entries.add( 0, currentChange );
200            status = GET_REVISION;
201            return;
202        }
203        
204        Matcher matcher = FILE_PATTERN.matcher( line );
205        if ( !matcher.find() )
206        {
207            return;
208        }
209
210        currentFile = matcher.group( 1 );
211
212        // Although Perforce allows files to be submitted anywhere in the
213        // repository in a single changelist, we're only concerned about the
214        // local files.
215        if( currentFile.startsWith( repoPath ) ) {
216            currentFile = currentFile.substring( repoPath.length() + 1 );
217            addEntry( currentChange, new ChangeFile( currentFile, matcher.group( 2 ) ) );
218        }
219    }
220
221    /**
222     * Most of the relevant info is on the revision line matching the
223     * 'pattern' string.
224     *
225     * @param line A line of text from the perforce log output
226     */
227    private void processGetRevision( String line )
228    {
229        Matcher matcher = REVISION_PATTERN.matcher( line );
230        if ( !matcher.find() )
231        {
232            return;
233        }
234        currentChange = new ChangeSet();
235        currentRevision = matcher.group( 1 );
236        currentChange.setAuthor( matcher.group( 2 ) );
237        currentChange.setDate( matcher.group( 3 ), userDatePattern );
238
239        status = GET_COMMENT_BEGIN;
240    }
241
242    /**
243     * Process the current input line in the GET_COMMENT state.  This
244     * state gathers all of the comments that are part of a log entry.
245     *
246     * @param line a line of text from the perforce log output
247     */
248    private void processGetComment( String line )
249    {
250        if ( line.equals( COMMENT_DELIMITER ) )
251        {
252            status = GET_AFFECTED_FILES;
253        }
254        else
255        {
256            // remove prepended tab
257            currentChange.setComment( currentChange.getComment() + line.substring(1) + "\n" );
258        }
259    }
260
261    /**
262     * Process the current input line in the GET_COMMENT state.  This
263     * state gathers all of the comments that are part of a log entry.
264     *
265     * @param line a line of text from the perforce log output
266     */
267    private void processGetAffectedFiles( String line )
268    {
269        if ( !line.equals( "Affected files ..." ) )
270        {
271            return;
272        }
273        status = GET_FILES_BEGIN;
274    }
275}