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 org.apache.maven.scm.ChangeFile;
023import org.apache.maven.scm.ChangeSet;
024import org.apache.maven.scm.ScmException;
025import org.apache.maven.scm.log.ScmLogger;
026import org.apache.maven.scm.util.AbstractConsumer;
027import org.apache.regexp.RE;
028import org.apache.regexp.RESyntaxException;
029
030import java.util.ArrayList;
031import java.util.Date;
032import java.util.LinkedHashMap;
033import java.util.List;
034import java.util.Map;
035
036/**
037 * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
038 *
039 */
040public class PerforceChangeLogConsumer
041    extends AbstractConsumer
042{
043    /**
044     * Date formatter for perforce timestamp
045     */
046    private static final String PERFORCE_TIMESTAMP_PATTERN = "yyyy/MM/dd HH:mm:ss";
047
048    private List<ChangeSet> entries = new ArrayList<ChangeSet>();
049
050    /**
051     * State machine constant: expecting revision and/or file information
052     */
053    private static final int GET_REVISION = 1;
054
055    /**
056     * State machine constant: eat the first blank line
057     */
058    private static final int GET_COMMENT_BEGIN = 2;
059
060    /**
061     * State machine constant: expecting comments
062     */
063    private static final int GET_COMMENT = 3;
064
065    /**
066     * The comment section ends with a blank line
067     */
068    private static final String COMMENT_DELIMITER = "";
069
070    /**
071     * A file line begins with two slashes
072     */
073    private static final String FILE_BEGIN_TOKEN = "//";
074
075    /**
076     * Current status of the parser
077     */
078    private int status = GET_REVISION;
079
080    /**
081     * The current log entry being processed by the parser
082     */
083    private ChangeSet currentChange;
084
085    /**
086     * the current file being processed by the parser
087     */
088    private String currentFile;
089
090    /**
091     * The location of files within the Perforce depot that we are processing
092     * e.g. //depot/projects/foo/bar
093     */
094    private String repoPath;
095
096    /**
097     * The regular expression used to match header lines
098     */
099    private RE revisionRegexp;
100
101    private Date startDate;
102
103    private Date endDate;
104
105    private String userDatePattern;
106
107    private static final String PATTERN = "^\\.\\.\\. #(\\d+) " + // revision number
108        "change (\\d+) .* " + // changelist number
109        "on (.*) " + // date
110        "by (.*)@"; // author
111
112    public PerforceChangeLogConsumer( String path, Date startDate, Date endDate, String userDatePattern,
113                                      ScmLogger logger )
114    {
115        super( logger );
116
117        this.startDate = startDate;
118        this.endDate = endDate;
119        this.userDatePattern = userDatePattern;
120        this.repoPath = path;
121
122        try
123        {
124            revisionRegexp = new RE( PATTERN );
125        }
126        catch ( RESyntaxException ignored )
127        {
128            if ( getLogger().isErrorEnabled() )
129            {
130                getLogger().error( "Could not create regexp to parse perforce log file", ignored );
131            }
132        }
133    }
134
135    // ----------------------------------------------------------------------
136    //
137    // ----------------------------------------------------------------------
138
139    public List<ChangeSet> getModifications() throws ScmException
140    {
141        
142        // Here there are one entry for each couple (changelist,file). We merge
143        // entries to have only one entry per changelist
144        
145        // Date > ChangeSet
146        Map<Date,ChangeSet> groupedEntries = new LinkedHashMap<Date,ChangeSet>();
147        for ( int i = 0; i < entries.size(); i++ )
148        {
149            ChangeSet cs = (ChangeSet) entries.get( i );
150            ChangeSet hit = (ChangeSet) groupedEntries.get( cs.getDate() );
151            if ( hit != null )
152            {
153                if ( cs.getFiles().size() != 1 )
154                {
155                    throw new ScmException( "Merge of entries failed. Bad entry size: " + cs.getFiles().size() );
156                }
157                hit.addFile( (ChangeFile) cs.getFiles().get( 0 ) );
158            }
159            else
160            {
161                groupedEntries.put( cs.getDate(), cs );
162            }
163        }
164
165        List<ChangeSet> result = new ArrayList<ChangeSet>();
166        result.addAll( groupedEntries.values() );
167
168        return result;
169        
170    }
171
172    // ----------------------------------------------------------------------
173    // StreamConsumer Implementation
174    // ----------------------------------------------------------------------
175
176    /** {@inheritDoc} */
177    public void consumeLine( String line )
178    {
179        switch ( status )
180        {
181            case GET_REVISION:
182                processGetRevision( line );
183                break;
184            case GET_COMMENT_BEGIN:
185                status = GET_COMMENT;
186                break;
187            case GET_COMMENT:
188                processGetComment( line );
189                break;
190            default:
191                throw new IllegalStateException( "Unknown state: " + status );
192        }
193    }
194
195    // ----------------------------------------------------------------------
196    //
197    // ----------------------------------------------------------------------
198
199    /**
200     * Add a change log entry to the list (if it's not already there)
201     * with the given file.
202     *
203     * @param entry a {@link ChangeSet} to be added to the list if another
204     *              with the same key (p4 change number) doesn't exist already.
205     * @param file  a {@link ChangeFile} to be added to the entry
206     */
207    private void addEntry( ChangeSet entry, ChangeFile file )
208    {
209        // ----------------------------------------------------------------------
210        // Check that we are inside the requested date range
211        // ----------------------------------------------------------------------
212
213        if ( startDate != null && entry.getDate().before( startDate ) )
214        {
215            return;
216        }
217
218        if ( endDate != null && entry.getDate().after( endDate ) )
219        {
220            return;
221        }
222
223        // ----------------------------------------------------------------------
224        //
225        // ----------------------------------------------------------------------
226
227        entry.addFile( file );
228
229        entries.add( entry );
230    }
231
232    /**
233     * Most of the relevant info is on the revision line matching the
234     * 'pattern' string.
235     *
236     * @param line A line of text from the perforce log output
237     */
238    private void processGetRevision( String line )
239    {
240        if ( line.startsWith( FILE_BEGIN_TOKEN ) )
241        {
242            currentFile = line.substring( repoPath.length() + 1 );
243            return;
244        }
245
246        if ( !revisionRegexp.match( line ) )
247        {
248            return;
249        }
250
251        currentChange = new ChangeSet();
252        currentChange.setDate( parseDate( revisionRegexp.getParen( 3 ), userDatePattern, PERFORCE_TIMESTAMP_PATTERN ) );
253        currentChange.setAuthor( revisionRegexp.getParen( 4 ) );
254
255        status = GET_COMMENT_BEGIN;
256    }
257
258    /**
259     * Process the current input line in the GET_COMMENT state.  This
260     * state gathers all of the comments that are part of a log entry.
261     *
262     * @param line a line of text from the perforce log output
263     */
264    private void processGetComment( String line )
265    {
266        if ( line.equals( COMMENT_DELIMITER ) )
267        {
268            addEntry( currentChange, new ChangeFile( currentFile, revisionRegexp.getParen( 1 ) ) );
269
270            status = GET_REVISION;
271        }
272        else
273        {
274            currentChange.setComment( currentChange.getComment() + line + "\n" );
275        }
276    }
277}