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 }