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        {
200            entries.add( 0, currentChange );
201            status = GET_REVISION;
202            return;
203        }
204
205        Matcher matcher = FILE_PATTERN.matcher( line );
206        if ( !matcher.find() )
207        {
208            return;
209        }
210
211        currentFile = matcher.group( 1 );
212
213        // Although Perforce allows files to be submitted anywhere in the
214        // repository in a single changelist, we're only concerned about the
215        // local files.
216        if ( currentFile.startsWith( repoPath ) )
217        {
218            currentFile = currentFile.substring( repoPath.length() + 1 );
219            addEntry( currentChange, new ChangeFile( currentFile, matcher.group( 2 ) ) );
220        }
221    }
222
223    /**
224     * Most of the relevant info is on the revision line matching the
225     * 'pattern' string.
226     *
227     * @param line A line of text from the perforce log output
228     */
229    private void processGetRevision( String line )
230    {
231        Matcher matcher = REVISION_PATTERN.matcher( line );
232        if ( !matcher.find() )
233        {
234            return;
235        }
236        currentChange = new ChangeSet();
237        currentRevision = matcher.group( 1 );
238        currentChange.setAuthor( matcher.group( 2 ) );
239        currentChange.setDate( matcher.group( 3 ), userDatePattern );
240
241        status = GET_COMMENT_BEGIN;
242    }
243
244    /**
245     * Process the current input line in the GET_COMMENT state.  This
246     * state gathers all of the comments that are part of a log entry.
247     *
248     * @param line a line of text from the perforce log output
249     */
250    private void processGetComment( String line )
251    {
252        if ( line.equals( COMMENT_DELIMITER ) )
253        {
254            status = GET_AFFECTED_FILES;
255        }
256        else
257        {
258            // remove prepended tab
259            currentChange.setComment( currentChange.getComment() + line.substring( 1 ) + "\n" );
260        }
261    }
262
263    /**
264     * Process the current input line in the GET_COMMENT state.  This
265     * state gathers all of the comments that are part of a log entry.
266     *
267     * @param line a line of text from the perforce log output
268     */
269    private void processGetAffectedFiles( String line )
270    {
271        if ( !line.equals( "Affected files ..." ) )
272        {
273            return;
274        }
275        status = GET_FILES_BEGIN;
276    }
277}