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