001 package org.apache.maven.scm.provider.jazz.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.ScmFileStatus;
025 import org.apache.maven.scm.log.ScmLogger;
026 import org.apache.maven.scm.provider.ScmProviderRepository;
027 import org.apache.maven.scm.provider.jazz.command.consumer.AbstractRepositoryConsumer;
028 import org.apache.regexp.RE;
029 import org.apache.regexp.RESyntaxException;
030
031 import java.util.ArrayList;
032 import java.util.Calendar;
033 import java.util.Date;
034 import java.util.List;
035 import java.util.Locale;
036
037 /**
038 * Consume the output of the scm command for the "list changesets" operation.
039 * <p/>
040 * This parses the contents of the output and uses it to fill in the remaining
041 * information in the <code>entries</code> list.
042 *
043 * @author <a href="mailto:ChrisGWarp@gmail.com">Chris Graham</a>
044 */
045 public class JazzListChangesetConsumer
046 extends AbstractRepositoryConsumer
047 {
048 //Change sets:
049 // (1589) ---$ Deb "[maven-release-plugin] prepare for next development iteration"
050 // Component: (1158) "GPDB"
051 // Modified: Feb 25, 2012 10:15 PM (Yesterday)
052 // Changes:
053 // ---c- (1170) \GPDB\GPDBEAR\pom.xml
054 // ---c- (1171) \GPDB\GPDBResources\pom.xml
055 // ---c- (1167) \GPDB\GPDBWeb\pom.xml
056 // ---c- (1165) \GPDB\pom.xml
057 // (1585) ---$ Deb "[maven-release-plugin] prepare release GPDB-1.0.21"
058 // Component: (1158) "GPDB"
059 // Modified: Feb 25, 2012 10:13 PM (Yesterday)
060 // Changes:
061 // ---c- (1170) \GPDB\GPDBEAR\pom.xml
062 // ---c- (1171) \GPDB\GPDBResources\pom.xml
063 // ---c- (1167) \GPDB\GPDBWeb\pom.xml
064 // ---c- (1165) \GPDB\pom.xml
065 // (1584) ---$ Deb "This is my first changeset (2)"
066 // Component: (1158) "GPDB"
067 // Modified: Feb 25, 2012 10:13 PM (Yesterday)
068 // (1583) ---$ Deb "This is my first changeset (1)"
069 // Component: (1158) "GPDB"
070 // Modified: Feb 25, 2012 10:13 PM (Yesterday)
071 // (1323) ---$ Deb <No comment>
072 // Component: (1158) "GPDB"
073 // Modified: Feb 24, 2012 11:04 PM (Last Week)
074 // Changes:
075 // ---c- (1170) \GPDB\GPDBEAR\pom.xml
076 // ---c- (1171) \GPDB\GPDBResources\pom.xml
077 // ---c- (1167) \GPDB\GPDBWeb\pom.xml
078 // ---c- (1165) \GPDB\pom.xml
079 // (1319) ---$ Deb <No comment>
080 // Component: (1158) "GPDB"
081 // Modified: Feb 24, 2012 11:03 PM (Last Week)
082 // Changes:
083 // ---c- (1170) \GPDB\GPDBEAR\pom.xml
084 // ---c- (1171) \GPDB\GPDBResources\pom.xml
085 // ---c- (1167) \GPDB\GPDBWeb\pom.xml
086 // ---c- (1165) \GPDB\pom.xml
087 //
088 // NOTE: If the change sets originate on the current date, the date is not
089 // displayed, only the time is.
090 // EG:
091 //Change sets:
092 // (1809) ---$ Deb "[maven-release-plugin] prepare for next development iteration"
093 // Component: (1158) "GPDB"
094 // Modified: 6:20 PM (5 minutes ago)
095 // Changes:
096 // ---c- (1170) \GPDB\GPDBEAR\pom.xml
097 // ---c- (1171) \GPDB\GPDBResources\pom.xml
098 // ---c- (1167) \GPDB\GPDBWeb\pom.xml
099 // ---c- (1165) \GPDB\pom.xml
100 // (1801) ---$ Deb "[maven-release-plugin] prepare release GPDB-1.0.26"
101 // Component: (1158) "GPDB"
102 // Modified: 6:18 PM (10 minutes ago)
103 // Changes:
104 // ---c- (1170) \GPDB\GPDBEAR\pom.xml
105 // ---c- (1171) \GPDB\GPDBResources\pom.xml
106 // ---c- (1167) \GPDB\GPDBWeb\pom.xml
107 // (1799) ---$ Deb <No comment>
108 // Component: (1158) "GPDB"
109 // Modified: 6:18 PM (10 minutes ago)
110 // Changes:
111 // ---c- (1165) \GPDB\pom.xml
112 // (1764) ---$ Deb <No comment>
113 // Component: (1158) "GPDB"
114 // Modified: Mar 1, 2012 2:34 PM
115 // Changes:
116 // ---c- (1165) \GPDB\pom.xml
117
118
119 // State Machine Definitions
120 private static final int STATE_CHANGE_SETS = 0;
121
122 private static final int STATE_CHANGE_SET = 1;
123
124 private static final int STATE_COMPONENT = 2;
125
126 private static final int STATE_MODIFIED = 3;
127
128 private static final int STATE_CHANGES = 4;
129
130 // Header definitions.
131 private static final String HEADER_CHANGE_SETS = "Change sets:";
132
133 private static final String HEADER_CHANGE_SET = "(";
134
135 private static final String HEADER_COMPONENT = "Component:";
136
137 private static final String HEADER_MODIFIED = "Modified:";
138
139 private static final String HEADER_CHANGES = "Changes:";
140
141 private static final String JAZZ_TIMESTAMP_PATTERN = "MMM d, yyyy h:mm a";
142 // Actually: DateFormat.getDateTimeInstance( DateFormat.MEDIUM, DateFormat.SHORT );
143
144 private static final String JAZZ_TIMESTAMP_PATTERN_TIME = "h:mm a";
145 // Only seen when the data = today. Only the time is displayed.
146
147 // (1589) ---$ Deb "[maven-release-plugin] prepare for next development iteration"
148 // (1585) ---$ Deb "[maven-release-plugin] prepare release GPDB-1.0.21"
149 private static final String CHANGESET_PATTERN = "\\((\\d+)\\) (....) (\\w+) (.*)";
150
151 /**
152 * @see #CHANGESET_PATTERN
153 */
154 private RE changeSetRegExp;
155
156 // ---c- (1170) \GPDB\GPDBEAR\pom.xml
157 // ---c- (1171) \GPDB\GPDBResources\pom.xml
158 // ---c- (1167) \GPDB\GPDBWeb\pom.xml
159 // ---c- (1165) \GPDB\pom.xml
160 private static final String CHANGES_PATTERN = "(.....) \\((\\d+)\\) (.*)";
161
162 /**
163 * @see #CHANGES_PATTERN
164 */
165 private RE changesRegExp;
166
167
168 private List<ChangeSet> entries;
169
170 private final String userDateFormat;
171
172 // This is incremented at the beginning of every change set line. So we start at -1 (to get zero on first processing)
173 private int currentChangeSetIndex = -1;
174
175 private int currentState = STATE_CHANGE_SETS;
176
177 /**
178 * Constructor for our "scm list changeset" consumer.
179 *
180 * @param repo The JazzScmProviderRepository being used.
181 * @param logger The ScmLogger to use.
182 * @param entries The List of ChangeSet entries that we will populate.
183 */
184 public JazzListChangesetConsumer( ScmProviderRepository repo, ScmLogger logger, List<ChangeSet> entries,
185 String userDateFormat )
186 {
187 super( repo, logger );
188 this.entries = entries;
189 this.userDateFormat = userDateFormat;
190
191 try
192 {
193 changeSetRegExp = new RE( CHANGESET_PATTERN );
194 changesRegExp = new RE( CHANGES_PATTERN );
195 }
196 catch ( RESyntaxException ex )
197 {
198 throw new RuntimeException(
199 "INTERNAL ERROR: Could not create regexp to parse jazz scm history output. This shouldn't happen. Something is probably wrong with the oro installation.",
200 ex );
201 }
202 }
203
204 /**
205 * Process one line of output from the execution of the "scm list changeset" command.
206 *
207 * @param line The line of output from the external command that has been pumped to us.
208 * @see org.codehaus.plexus.util.cli.StreamConsumer#consumeLine(java.lang.String)
209 */
210 public void consumeLine( String line )
211 {
212 super.consumeLine( line );
213
214 // Process the "Change sets:" line - do nothing
215 if ( line.trim().startsWith( HEADER_CHANGE_SETS ) )
216 {
217 currentState = STATE_CHANGE_SETS;
218 }
219 else
220 {
221 if ( line.trim().startsWith( HEADER_CHANGE_SET ) )
222 {
223 currentState = STATE_CHANGE_SET;
224 }
225 else
226 {
227 if ( line.trim().startsWith( HEADER_COMPONENT ) )
228 {
229 currentState = STATE_COMPONENT;
230 }
231 else
232 {
233 if ( line.trim().startsWith( HEADER_MODIFIED ) )
234 {
235 currentState = STATE_MODIFIED;
236 }
237 else
238 {
239 if ( line.trim().startsWith( HEADER_CHANGES ) )
240 {
241 // Note: processChangesLine() will also be passed the "Changes:" line
242 // So, it needs to be able to deal with that.
243 // Changes:
244 // ---c- (1170) \GPDB\GPDBEAR\pom.xml
245 // ---c- (1171) \GPDB\GPDBResources\pom.xml
246 // ---c- (1167) \GPDB\GPDBWeb\pom.xml
247 // ---c- (1165) \GPDB\pom.xml
248 currentState = STATE_CHANGES;
249 }
250 }
251 }
252 }
253 }
254
255 switch ( currentState )
256 {
257 case STATE_CHANGE_SETS:
258 // Nothing to do.
259 break;
260
261 case STATE_CHANGE_SET:
262 processChangeSetLine( line );
263 break;
264
265 case STATE_COMPONENT:
266 // Nothing to do. Not used (Yet?)
267 break;
268
269 case STATE_MODIFIED:
270 processModifiedLine( line );
271 break;
272
273 case STATE_CHANGES:
274 processChangesLine( line );
275 break;
276 }
277
278 }
279
280 private void processChangeSetLine( String line )
281 {
282 // Process the headerless change set line - starts with a '(', eg:
283 // (1589) ---$ Deb "[maven-release-plugin] prepare for next development iteration"
284 // (1585) ---$ Deb "[maven-release-plugin] prepare release GPDB-1.0.21"
285 if ( changeSetRegExp.match( line ) )
286 {
287 // This is the only place this gets incremented.
288 // It starts at -1, and on first execution is incremented to 0 - which is correct.
289 currentChangeSetIndex++;
290 ChangeSet currentChangeSet = entries.get( currentChangeSetIndex );
291
292 // Init the file of files, so it is not null, but it can be empty!
293 List<ChangeFile> files = new ArrayList<ChangeFile>();
294 currentChangeSet.setFiles( files );
295
296 String changesetAlias = changeSetRegExp.getParen( 1 );
297 String changeFlags = changeSetRegExp.getParen( 2 ); // Not used.
298 String author = changeSetRegExp.getParen( 3 );
299 String comment = changeSetRegExp.getParen( 4 );
300
301 if ( getLogger().isDebugEnabled() )
302 {
303 getLogger().debug( " Parsing ChangeSet Line : " + line );
304 getLogger().debug( " changesetAlias : " + changesetAlias );
305 getLogger().debug( " changeFlags : " + changeFlags );
306 getLogger().debug( " author : " + author );
307 getLogger().debug( " comment : " + comment );
308 }
309
310 // Sanity check.
311 if ( currentChangeSet.getRevision() != null && !currentChangeSet.getRevision().equals( changesetAlias ) )
312 {
313 getLogger().warn( "Warning! The indexes appear to be out of sequence! " +
314 "For currentChangeSetIndex = " + currentChangeSetIndex + ", we got '" +
315 changesetAlias + "' and not '" + currentChangeSet.getRevision()
316 + "' as expected." );
317 }
318
319 comment = stripDelimiters( comment );
320 currentChangeSet.setAuthor( author );
321 currentChangeSet.setComment( comment );
322 }
323 }
324
325 private void processModifiedLine( String line )
326 {
327 // Process the "Modified: ..." line, eg:
328 // Modified: Feb 25, 2012 10:15 PM (Yesterday)
329 // Modified: Feb 25, 2012 10:13 PM (Yesterday)
330 // Modified: Feb 24, 2012 11:03 PM (Last Week)
331 // Modified: Mar 1, 2012 2:34 PM
332 // Modified: 6:20 PM (5 minutes ago)
333
334 if ( getLogger().isDebugEnabled() )
335 {
336 getLogger().debug( " Parsing Modified Line : " + line );
337 }
338
339 int colonPos = line.indexOf( ":" );
340 int parenPos = line.indexOf( "(" );
341
342 String date = null;
343
344 if ( colonPos != -1 && parenPos != -1 )
345 {
346 date = line.substring( colonPos + 2, parenPos - 1 );
347 }
348 else
349 {
350 if ( colonPos != -1 && parenPos == -1 )
351 {
352 // No trailing bracket
353 date = line.substring( colonPos + 2 );
354 }
355 }
356
357 if ( date != null )
358 {
359 Date changesetDate = parseDate( date.toString(), userDateFormat, JAZZ_TIMESTAMP_PATTERN );
360 // try again forcing en locale
361 if ( changesetDate == null )
362 {
363 changesetDate = parseDate( date.toString(), userDateFormat, JAZZ_TIMESTAMP_PATTERN, Locale.ENGLISH );
364 }
365 if ( changesetDate == null )
366 {
367 // changesetDate will be null when the date is not given, it only has just the time. The date is today.
368 changesetDate = parseDate( date.toString(), userDateFormat, JAZZ_TIMESTAMP_PATTERN_TIME );
369 // Get today's time/date. Used to get the date.
370 Calendar today = Calendar.getInstance();
371 // Get a working one.
372 Calendar changesetCal = Calendar.getInstance();
373 // Set the date/time. Used to set the time.
374 changesetCal.setTimeInMillis( changesetDate.getTime() );
375 // Now set the date (today).
376 changesetCal.set( today.get( Calendar.YEAR ), today.get( Calendar.MONTH ),
377 today.get( Calendar.DAY_OF_MONTH ) );
378 // Now get the date of the combined results.
379 changesetDate = changesetCal.getTime();
380 }
381
382 if ( getLogger().isDebugEnabled() )
383 {
384 getLogger().debug( " date : " + date );
385 getLogger().debug( " changesetDate : " + changesetDate );
386 }
387
388 ChangeSet currentChangeSet = entries.get( currentChangeSetIndex );
389 currentChangeSet.setDate( changesetDate );
390 }
391 }
392
393 private void processChangesLine( String line )
394 {
395 // Process the changes line, eg:
396 // ---c- (1170) \GPDB\GPDBEAR\pom.xml
397 // ---c- (1171) \GPDB\GPDBResources\pom.xml
398 // ---c- (1167) \GPDB\GPDBWeb\pom.xml
399 // ---c- (1165) \GPDB\pom.xml
400 if ( changesRegExp.match( line ) )
401 {
402 ChangeSet currentChangeSet = entries.get( currentChangeSetIndex );
403
404 String changeFlags = changesRegExp.getParen( 1 ); // Not used.
405 String fileAlias = changesRegExp.getParen( 2 );
406 String file = changesRegExp.getParen( 3 );
407
408 if ( getLogger().isDebugEnabled() )
409 {
410 getLogger().debug( " Parsing Changes Line : " + line );
411 getLogger().debug(
412 " changeFlags : " + changeFlags + " Translated to : " + parseFileChangeState( changeFlags ) );
413 getLogger().debug( " filetAlias : " + fileAlias );
414 getLogger().debug( " file : " + file );
415 }
416
417 ChangeFile changeFile = new ChangeFile( file );
418 ScmFileStatus status = parseFileChangeState( changeFlags );
419 changeFile.setAction( status );
420 currentChangeSet.getFiles().add( changeFile );
421 }
422 }
423
424 /**
425 * String the leading/trailing ", < and > from the text.
426 *
427 * @param text The text to process.
428 * @return The striped text.
429 */
430 protected String stripDelimiters( String text )
431 {
432 if ( text == null )
433 {
434 return null;
435 }
436 String workingText = text;
437 if ( workingText.startsWith( "\"" ) || workingText.startsWith( "<" ) )
438 {
439 workingText = workingText.substring( 1 );
440 }
441 if ( workingText.endsWith( "\"" ) || workingText.endsWith( ">" ) )
442 {
443 workingText = workingText.substring( 0, workingText.length() - 1 );
444 }
445
446 return workingText;
447 }
448
449 /**
450 * Parse the change state file flags from Jazz and map them to the maven SCM ones.
451 * <p/>
452 * "----" Character positions 0-3.
453 * <p/>
454 * [0] is '*' or '-' Indicates that this is the current change set ('*') or not ('-'). STATE_CHANGESET_CURRENT
455 * [1] is '!' or '-' Indicates a Potential Conflict ('!') or not ('-'). STATE_POTENTIAL_CONFLICT
456 * [2] is '#' or '-' Indicates a Conflict ('#') or not ('-'). STATE_CONFLICT
457 * [3] is '@' or '$' Indicates whether the changeset is active ('@') or not ('$'). STATE_CHANGESET_ACTIVE
458 *
459 * @param state The 5 character long state string
460 * @return The ScmFileStatus value.
461 */
462 private ScmFileStatus parseChangeSetChangeState( String state )
463 {
464 if ( state.length() != 4 )
465 {
466 throw new IllegalArgumentException( "Change State string must be 4 chars long!" );
467 }
468
469 // This is not used, but is here for potential future usage and for documentation purposes.
470 return ScmFileStatus.UNKNOWN;
471 }
472
473 /**
474 * Parse the change state file flags from Jazz and map them to the maven SCM ones.
475 * <p/>
476 * "-----" Character positions 0-4. The default is '-'.
477 * <p/>
478 * [0] is '-' or '!' Indicates a Potential Conflict. STATE_POTENTIAL_CONFLICT
479 * [1] is '-' or '#' Indicates a Conflict. STATE_CONFLICT
480 * [2] is '-' or 'a' Indicates an addition. STATE_ADD
481 * or 'd' Indicates a deletion. STATE_DELETE
482 * or 'm' Indicates a move. STATE_MOVE
483 * [3] is '-' or 'c' Indicates a content change. STATE_CONTENT_CHANGE
484 * [4] is '-' or 'p' Indicates a property change. STATE_PROPERTY_CHANGE
485 * <p/>
486 * NOTE: [3] and [4] can only be set it [2] is NOT 'a' or 'd'.
487 *
488 * @param state The 5 character long state string
489 * @return The SCMxxx value.
490 */
491 private ScmFileStatus parseFileChangeState( String state )
492 {
493 if ( state.length() != 5 )
494 {
495 throw new IllegalArgumentException( "Change State string must be 5 chars long!" );
496 }
497
498 // NOTE: We have an impedance mismatch here. The Jazz file change flags represent
499 // many different states. However, we can only return *ONE* ScmFileStatus value,
500 // so we need to be careful as to the precedence that we give to them.
501
502 ScmFileStatus status = ScmFileStatus.UNKNOWN; // Probably not a valid initial default value.
503
504 // [0] is '-' or '!' Indicates a Potential Conflict. STATE_POTENTIAL_CONFLICT
505 if ( state.charAt( 0 ) == '!' )
506 {
507 status = ScmFileStatus.CONFLICT;
508 }
509 // [1] is '-' or '#' Indicates a Conflict. STATE_CONFLICT
510 if ( state.charAt( 1 ) == '#' )
511 {
512 status = ScmFileStatus.CONFLICT;
513 }
514
515 // [2] is '-' or 'a' Indicates an addition. STATE_ADD
516 // or 'd' Indicates a deletion. STATE_DELETE
517 // or 'm' Indicates a move. STATE_MOVE
518 if ( state.charAt( 2 ) == 'a' )
519 {
520 status = ScmFileStatus.ADDED;
521 }
522 else
523 {
524 if ( state.charAt( 2 ) == 'd' )
525 {
526 status = ScmFileStatus.DELETED;
527 }
528 else
529 {
530 if ( state.charAt( 2 ) == 'm' )
531 {
532 status = ScmFileStatus.RENAMED; // Has been renamed or moved.
533 }
534
535 // [3] is '-' or 'c' Indicates a content change. STATE_CONTENT_CHANGE
536 if ( state.charAt( 3 ) == 'c' )
537 {
538 status = ScmFileStatus.MODIFIED; // The file has been modified in the working tree.
539 }
540
541 // [4] is '-' or 'p' Indicates a property change. STATE_PROPERTY_CHANGE
542 if ( state.charAt( 4 ) == 'p' )
543 {
544 status =
545 ScmFileStatus.MODIFIED; // ScmFileStatus has no concept of property or meta data changes.
546 }
547 }
548 }
549
550 return status;
551 }
552 }