1 package org.apache.maven.perforcelib;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 import java.io.BufferedReader;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.InputStreamReader;
25
26 import java.text.ParseException;
27 import java.text.SimpleDateFormat;
28
29 import java.util.Collection;
30 import java.util.Collections;
31 import java.util.Date;
32 import java.util.Map;
33 import java.util.TreeMap;
34
35 import org.apache.commons.logging.Log;
36 import org.apache.commons.logging.LogFactory;
37 import org.apache.maven.changelog.ChangeLog;
38 import org.apache.maven.changelog.ChangeLogEntry;
39 import org.apache.maven.changelog.ChangeLogFile;
40 import org.apache.maven.changelog.ChangeLogParser;
41 import org.apache.regexp.RE;
42 import org.apache.regexp.RESyntaxException;
43
44
45 /**
46 * A class to parse the log output from the Perforce 'filelog'
47 * command.
48 *
49 * @author <a href="mailto:jim@crossleys.org">Jim Crossley</a>
50 * @version $Id:
51 */
52 public class PerforceChangeLogParser implements ChangeLogParser
53 {
54 /** Date formatter for perforce timestamp */
55 private static final SimpleDateFormat PERFORCE_TIMESTAMP =
56 new SimpleDateFormat( "yyyy/MM/dd HH:mm:ss" );
57
58 /** Log */
59 private static final Log LOG =
60 LogFactory.getLog( PerforceChangeLogParser.class );
61
62 /** State machine constant: expecting revision and/or file information */
63 private static final int GET_REVISION = 1;
64
65 /** State machine constant: eat the first blank line */
66 private static final int GET_COMMENT_BEGIN = 2;
67
68 /** State machine constant: expecting comments */
69 private static final int GET_COMMENT = 3;
70
71 /** The comment section ends with a blank line */
72 private static final String COMMENT_DELIMITER = "";
73
74 /** A file line begins with two slashes */
75 private static final String FILE_BEGIN_TOKEN = "//";
76 private static final String pattern =
77 "^\\.\\.\\. #(\\d+) "
78 + "change (\\d+) .* "
79 + "on (.*) "
80 + "by (.*)@";
81
82 /**
83 * RCS entries, in reverse changelist number order
84 */
85 private Map entries = new TreeMap( Collections.reverseOrder() );
86
87 /** Current status of the parser */
88 private int status = GET_REVISION;
89
90 /** The current log entry being processed by the parser */
91 private ChangeLogEntry currentLogEntry;
92
93 /** the current file being processed by the parser */
94 private String currentFile;
95
96 /** The regular expression used to match header lines */
97 private RE revisionRegexp;
98
99 /** The invoking changelog controller (useful to log messages) */
100 private ChangeLog changeLog;
101
102 /** the before date */
103 private Date beforeDate;
104
105 /** The depot filespec will be stripped from each filename */
106 private int prefixLength;
107
108 /**
109 * Default constructor.
110 */
111 public PerforceChangeLogParser()
112 {
113 try
114 {
115 revisionRegexp = new RE( pattern );
116 }
117 catch ( RESyntaxException ignored )
118 {
119 LOG.error( "Could not create regexp to parse perforce log file",
120 ignored );
121 }
122 }
123
124 /**
125 * Initialize the parser from the change log.
126 *
127 * @param changeLog The controlling task
128 * @see ChangeLogParser#init(ChangeLog)
129 */
130 public void init( ChangeLog changeLog )
131 {
132 this.changeLog = changeLog;
133 setDateRange();
134
135 String conn = changeLog.getRepositoryConnection();
136 String filespec = conn.substring( conn.lastIndexOf( ':' ) + 1 );
137
138 this.prefixLength = 1 + filespec.lastIndexOf( '/' );
139 }
140
141 /**
142 * Clean up any parser resources.
143 *
144 * @see ChangeLogParser#cleanup()
145 */
146 public void cleanup()
147 {
148 }
149
150 /**
151 * Parse the input stream into a collection of entries
152 *
153 * @param anInputStream An input stream containing perforce log output
154 * @return A collection of ChangeLogEntry's
155 * @throws IOException When there are errors reading the provided stream
156 */
157 public Collection parse( InputStream anInputStream )
158 throws IOException
159 {
160 BufferedReader stream =
161 new BufferedReader( new InputStreamReader( anInputStream ) );
162
163 String line = null;
164
165 while ( ( line = stream.readLine() ) != null )
166 {
167 switch ( status )
168 {
169 case GET_REVISION :
170 processGetRevision( line );
171
172 break;
173
174 case GET_COMMENT_BEGIN :
175 status = GET_COMMENT;
176
177 break;
178
179 case GET_COMMENT :
180 processGetComment( line );
181
182 break;
183
184 default :
185 throw new IllegalStateException( "Unknown state: " + status );
186 }
187 }
188
189 return entries.values();
190 }
191
192 /**
193 * Add a change log entry to the list (if it's not already there)
194 * with the given file.
195 * @param entry a {@link ChangeLogEntry} to be added to the list if another
196 * with the same key (p4 change number) doesn't exist already.
197 * @param file a {@link ChangeLogFile} to be added to the entry
198 */
199 private void addEntry( ChangeLogEntry entry, ChangeLogFile file )
200 {
201 if ( beforeDate != null )
202 {
203 if ( entry.getDate().before( beforeDate ) )
204 {
205 return;
206 }
207 }
208
209 Integer key = new Integer( revisionRegexp.getParen( 2 ) );
210
211 if ( !entries.containsKey( key ) )
212 {
213 entry.addFile( file );
214 entries.put( key, entry );
215 }
216 else
217 {
218 ChangeLogEntry existingEntry = (ChangeLogEntry) entries.get( key );
219
220 existingEntry.addFile( file );
221 }
222 }
223
224 /**
225 * Most of the relevant info is on the revision line matching the
226 * 'pattern' string.
227 *
228 * @param line A line of text from the perforce log output
229 */
230 private void processGetRevision( String line )
231 {
232 if ( line.startsWith( FILE_BEGIN_TOKEN ) )
233 {
234 currentFile = line.substring( this.prefixLength );
235
236 return;
237 }
238
239 if ( !revisionRegexp.match( line ) )
240 {
241 return;
242 }
243
244 currentLogEntry = new ChangeLogEntry();
245 currentLogEntry.setDate( parseDate( revisionRegexp.getParen( 3 ) ) );
246 currentLogEntry.setAuthor( revisionRegexp.getParen( 4 ) );
247
248 status = GET_COMMENT_BEGIN;
249 }
250
251 /**
252 * Process the current input line in the GET_COMMENT state. This
253 * state gathers all of the comments that are part of a log entry.
254 *
255 * @param line a line of text from the perforce log output
256 */
257 private void processGetComment( String line )
258 {
259 if ( line.equals( COMMENT_DELIMITER ) )
260 {
261 addEntry( currentLogEntry,
262 new ChangeLogFile( currentFile, revisionRegexp.getParen( 1 ) ) );
263 status = GET_REVISION;
264 }
265 else
266 {
267 currentLogEntry.setComment( currentLogEntry.getComment() + line
268 + "\n" );
269 }
270 }
271
272 /**
273 * Converts the date timestamp from the perforce output into a date
274 * object.
275 *
276 * @return A date representing the timestamp of the log entry.
277 */
278 private Date parseDate( String date )
279 {
280 try
281 {
282 return PERFORCE_TIMESTAMP.parse( date );
283 }
284 catch ( ParseException e )
285 {
286 LOG.error( "ParseException Caught", e );
287
288 return null;
289 }
290 }
291
292 /**
293 * Set the beforeDate member based on the number of days obtained
294 * from the ChangeLog.
295 *
296 * @param numDaysString The number of days of log output to
297 * generate.
298 */
299 private void setDateRange()
300 {
301 if ( ( this.changeLog == null )
302 || ( this.changeLog.getRange() == null )
303 || ( this.changeLog.getRange().length() == 0 ) )
304 {
305 return;
306 }
307
308 int days = Integer.parseInt( this.changeLog.getRange() );
309
310 beforeDate =
311 new Date( System.currentTimeMillis()
312 - ( (long) days * 24 * 60 * 60 * 1000 ) );
313 }
314
315 public void setDateFormatInFile( String dateFormat )
316 {
317 }
318 }