001package 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 022import org.apache.maven.scm.ChangeFile; 023import org.apache.maven.scm.ChangeSet; 024import org.apache.maven.scm.ScmFileStatus; 025import org.apache.maven.scm.log.ScmLogger; 026import org.apache.maven.scm.provider.ScmProviderRepository; 027import org.apache.maven.scm.provider.jazz.command.consumer.AbstractRepositoryConsumer; 028 029import java.util.ArrayList; 030import java.util.Calendar; 031import java.util.Date; 032import java.util.List; 033import java.util.Locale; 034import java.util.regex.Matcher; 035import java.util.regex.Pattern; 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 */ 045public 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 Pattern CHANGESET_PATTERN = Pattern.compile( "\\((\\d+)\\) (....) (\\w+) (.*)" ); 150 151 // ---c- (1170) \GPDB\GPDBEAR\pom.xml 152 // ---c- (1171) \GPDB\GPDBResources\pom.xml 153 // ---c- (1167) \GPDB\GPDBWeb\pom.xml 154 // ---c- (1165) \GPDB\pom.xml 155 private static final Pattern CHANGES_PATTERN = Pattern.compile( "(.....) \\((\\d+)\\) (.*)" ); 156 157 158 private List<ChangeSet> entries; 159 160 private final String userDateFormat; 161 162 // This is incremented at the beginning of every change set line. So we start at -1 (to get zero on first 163 // processing) 164 private int currentChangeSetIndex = -1; 165 166 private int currentState = STATE_CHANGE_SETS; 167 168 /** 169 * Constructor for our "scm list changeset" consumer. 170 * 171 * @param repo The JazzScmProviderRepository being used. 172 * @param logger The ScmLogger to use. 173 * @param entries The List of ChangeSet entries that we will populate. 174 */ 175 public JazzListChangesetConsumer( ScmProviderRepository repo, ScmLogger logger, List<ChangeSet> entries, 176 String userDateFormat ) 177 { 178 super( repo, logger ); 179 this.entries = entries; 180 this.userDateFormat = userDateFormat; 181 } 182 183 /** 184 * Process one line of output from the execution of the "scm list changeset" command. 185 * 186 * @param line The line of output from the external command that has been pumped to us. 187 * @see org.codehaus.plexus.util.cli.StreamConsumer#consumeLine(java.lang.String) 188 */ 189 public void consumeLine( String line ) 190 { 191 super.consumeLine( line ); 192 193 // Process the "Change sets:" line - do nothing 194 if ( line.trim().startsWith( HEADER_CHANGE_SETS ) ) 195 { 196 currentState = STATE_CHANGE_SETS; 197 } 198 else 199 { 200 if ( line.trim().startsWith( HEADER_CHANGE_SET ) ) 201 { 202 currentState = STATE_CHANGE_SET; 203 } 204 else 205 { 206 if ( line.trim().startsWith( HEADER_COMPONENT ) ) 207 { 208 currentState = STATE_COMPONENT; 209 } 210 else 211 { 212 if ( line.trim().startsWith( HEADER_MODIFIED ) ) 213 { 214 currentState = STATE_MODIFIED; 215 } 216 else 217 { 218 if ( line.trim().startsWith( HEADER_CHANGES ) ) 219 { 220 // Note: processChangesLine() will also be passed the "Changes:" line 221 // So, it needs to be able to deal with that. 222 // Changes: 223 // ---c- (1170) \GPDB\GPDBEAR\pom.xml 224 // ---c- (1171) \GPDB\GPDBResources\pom.xml 225 // ---c- (1167) \GPDB\GPDBWeb\pom.xml 226 // ---c- (1165) \GPDB\pom.xml 227 currentState = STATE_CHANGES; 228 } 229 } 230 } 231 } 232 } 233 234 switch ( currentState ) 235 { 236 case STATE_CHANGE_SETS: 237 // Nothing to do. 238 break; 239 240 case STATE_CHANGE_SET: 241 processChangeSetLine( line ); 242 break; 243 244 case STATE_COMPONENT: 245 // Nothing to do. Not used (Yet?) 246 break; 247 248 case STATE_MODIFIED: 249 processModifiedLine( line ); 250 break; 251 252 case STATE_CHANGES: 253 processChangesLine( line ); 254 break; 255 256 default: 257 } 258 259 } 260 261 private void processChangeSetLine( String line ) 262 { 263 // Process the headerless change set line - starts with a '(', eg: 264 // (1589) ---$ Deb "[maven-release-plugin] prepare for next development iteration" 265 // (1585) ---$ Deb "[maven-release-plugin] prepare release GPDB-1.0.21" 266 Matcher matcher = CHANGESET_PATTERN.matcher( line ); 267 if ( matcher.find() ) 268 { 269 // This is the only place this gets incremented. 270 // It starts at -1, and on first execution is incremented to 0 - which is correct. 271 currentChangeSetIndex++; 272 ChangeSet currentChangeSet = entries.get( currentChangeSetIndex ); 273 274 // Init the file of files, so it is not null, but it can be empty! 275 List<ChangeFile> files = new ArrayList<ChangeFile>(); 276 currentChangeSet.setFiles( files ); 277 278 String changesetAlias = matcher.group( 1 ); 279 String changeFlags = matcher.group( 2 ); // Not used. 280 String author = matcher.group( 3 ); 281 String comment = matcher.group( 4 ); 282 283 if ( getLogger().isDebugEnabled() ) 284 { 285 getLogger().debug( " Parsing ChangeSet Line : " + line ); 286 getLogger().debug( " changesetAlias : " + changesetAlias ); 287 getLogger().debug( " changeFlags : " + changeFlags ); 288 getLogger().debug( " author : " + author ); 289 getLogger().debug( " comment : " + comment ); 290 } 291 292 // Sanity check. 293 if ( currentChangeSet.getRevision() != null && !currentChangeSet.getRevision().equals( changesetAlias ) ) 294 { 295 getLogger().warn( "Warning! The indexes appear to be out of sequence! " 296 + "For currentChangeSetIndex = " + currentChangeSetIndex + ", we got '" 297 + changesetAlias + "' and not '" + currentChangeSet.getRevision() 298 + "' as expected." ); 299 } 300 301 comment = stripDelimiters( comment ); 302 currentChangeSet.setAuthor( author ); 303 currentChangeSet.setComment( comment ); 304 } 305 } 306 307 private void processModifiedLine( String line ) 308 { 309 // Process the "Modified: ..." line, eg: 310 // Modified: Feb 25, 2012 10:15 PM (Yesterday) 311 // Modified: Feb 25, 2012 10:13 PM (Yesterday) 312 // Modified: Feb 24, 2012 11:03 PM (Last Week) 313 // Modified: Mar 1, 2012 2:34 PM 314 // Modified: 6:20 PM (5 minutes ago) 315 316 if ( getLogger().isDebugEnabled() ) 317 { 318 getLogger().debug( " Parsing Modified Line : " + line ); 319 } 320 321 int colonPos = line.indexOf( ":" ); 322 int parenPos = line.indexOf( "(" ); 323 324 String date = null; 325 326 if ( colonPos != -1 && parenPos != -1 ) 327 { 328 date = line.substring( colonPos + 2, parenPos - 1 ); 329 } 330 else 331 { 332 if ( colonPos != -1 && parenPos == -1 ) 333 { 334 // No trailing bracket 335 date = line.substring( colonPos + 2 ); 336 } 337 } 338 339 if ( date != null ) 340 { 341 Date changesetDate = parseDate( date.toString(), userDateFormat, JAZZ_TIMESTAMP_PATTERN ); 342 // try again forcing en locale 343 if ( changesetDate == null ) 344 { 345 changesetDate = parseDate( date.toString(), userDateFormat, JAZZ_TIMESTAMP_PATTERN, Locale.ENGLISH ); 346 } 347 if ( changesetDate == null ) 348 { 349 // changesetDate will be null when the date is not given, it only has just the time. The date is today. 350 changesetDate = parseDate( date.toString(), userDateFormat, JAZZ_TIMESTAMP_PATTERN_TIME ); 351 // Get today's time/date. Used to get the date. 352 Calendar today = Calendar.getInstance(); 353 // Get a working one. 354 Calendar changesetCal = Calendar.getInstance(); 355 // Set the date/time. Used to set the time. 356 changesetCal.setTimeInMillis( changesetDate.getTime() ); 357 // Now set the date (today). 358 changesetCal.set( today.get( Calendar.YEAR ), today.get( Calendar.MONTH ), 359 today.get( Calendar.DAY_OF_MONTH ) ); 360 // Now get the date of the combined results. 361 changesetDate = changesetCal.getTime(); 362 } 363 364 if ( getLogger().isDebugEnabled() ) 365 { 366 getLogger().debug( " date : " + date ); 367 getLogger().debug( " changesetDate : " + changesetDate ); 368 } 369 370 ChangeSet currentChangeSet = entries.get( currentChangeSetIndex ); 371 currentChangeSet.setDate( changesetDate ); 372 } 373 } 374 375 private void processChangesLine( String line ) 376 { 377 // Process the changes line, eg: 378 // ---c- (1170) \GPDB\GPDBEAR\pom.xml 379 // ---c- (1171) \GPDB\GPDBResources\pom.xml 380 // ---c- (1167) \GPDB\GPDBWeb\pom.xml 381 // ---c- (1165) \GPDB\pom.xml 382 Matcher matcher = CHANGES_PATTERN.matcher( line ); 383 if ( matcher.find() ) 384 { 385 ChangeSet currentChangeSet = entries.get( currentChangeSetIndex ); 386 387 String changeFlags = matcher.group( 1 ); // Not used. 388 String fileAlias = matcher.group( 2 ); 389 String file = matcher.group( 3 ); 390 391 if ( getLogger().isDebugEnabled() ) 392 { 393 getLogger().debug( " Parsing Changes Line : " + line ); 394 getLogger().debug( 395 " changeFlags : " + changeFlags + " Translated to : " + parseFileChangeState( changeFlags ) ); 396 getLogger().debug( " filetAlias : " + fileAlias ); 397 getLogger().debug( " file : " + file ); 398 } 399 400 ChangeFile changeFile = new ChangeFile( file ); 401 ScmFileStatus status = parseFileChangeState( changeFlags ); 402 changeFile.setAction( status ); 403 currentChangeSet.getFiles().add( changeFile ); 404 } 405 } 406 407 /** 408 * String the leading/trailing ", < and > from the text. 409 * 410 * @param text The text to process. 411 * @return The striped text. 412 */ 413 protected String stripDelimiters( String text ) 414 { 415 if ( text == null ) 416 { 417 return null; 418 } 419 String workingText = text; 420 if ( workingText.startsWith( "\"" ) || workingText.startsWith( "<" ) ) 421 { 422 workingText = workingText.substring( 1 ); 423 } 424 if ( workingText.endsWith( "\"" ) || workingText.endsWith( ">" ) ) 425 { 426 workingText = workingText.substring( 0, workingText.length() - 1 ); 427 } 428 429 return workingText; 430 } 431 432 /** 433 * Parse the change state file flags from Jazz and map them to the maven SCM ones. 434 * <p/> 435 * "----" Character positions 0-3. 436 * <p/> 437 * [0] is '*' or '-' Indicates that this is the current change set ('*') or not ('-'). STATE_CHANGESET_CURRENT 438 * [1] is '!' or '-' Indicates a Potential Conflict ('!') or not ('-'). STATE_POTENTIAL_CONFLICT 439 * [2] is '#' or '-' Indicates a Conflict ('#') or not ('-'). STATE_CONFLICT 440 * [3] is '@' or '$' Indicates whether the changeset is active ('@') or not ('$'). STATE_CHANGESET_ACTIVE 441 * 442 * @param state The 5 character long state string 443 * @return The ScmFileStatus value. 444 */ 445 private ScmFileStatus parseChangeSetChangeState( String state ) 446 { 447 if ( state.length() != 4 ) 448 { 449 throw new IllegalArgumentException( "Change State string must be 4 chars long!" ); 450 } 451 452 // This is not used, but is here for potential future usage and for documentation purposes. 453 return ScmFileStatus.UNKNOWN; 454 } 455 456 /** 457 * Parse the change state file flags from Jazz and map them to the maven SCM ones. 458 * <p/> 459 * "-----" Character positions 0-4. The default is '-'. 460 * <p/> 461 * [0] is '-' or '!' Indicates a Potential Conflict. STATE_POTENTIAL_CONFLICT 462 * [1] is '-' or '#' Indicates a Conflict. STATE_CONFLICT 463 * [2] is '-' or 'a' Indicates an addition. STATE_ADD 464 * or 'd' Indicates a deletion. STATE_DELETE 465 * or 'm' Indicates a move. STATE_MOVE 466 * [3] is '-' or 'c' Indicates a content change. STATE_CONTENT_CHANGE 467 * [4] is '-' or 'p' Indicates a property change. STATE_PROPERTY_CHANGE 468 * <p/> 469 * NOTE: [3] and [4] can only be set it [2] is NOT 'a' or 'd'. 470 * 471 * @param state The 5 character long state string 472 * @return The SCMxxx value. 473 */ 474 private ScmFileStatus parseFileChangeState( String state ) 475 { 476 if ( state.length() != 5 ) 477 { 478 throw new IllegalArgumentException( "Change State string must be 5 chars long!" ); 479 } 480 481 // NOTE: We have an impedance mismatch here. The Jazz file change flags represent 482 // many different states. However, we can only return *ONE* ScmFileStatus value, 483 // so we need to be careful as to the precedence that we give to them. 484 485 ScmFileStatus status = ScmFileStatus.UNKNOWN; // Probably not a valid initial default value. 486 487 // [0] is '-' or '!' Indicates a Potential Conflict. STATE_POTENTIAL_CONFLICT 488 if ( state.charAt( 0 ) == '!' ) 489 { 490 status = ScmFileStatus.CONFLICT; 491 } 492 // [1] is '-' or '#' Indicates a Conflict. STATE_CONFLICT 493 if ( state.charAt( 1 ) == '#' ) 494 { 495 status = ScmFileStatus.CONFLICT; 496 } 497 498 // [2] is '-' or 'a' Indicates an addition. STATE_ADD 499 // or 'd' Indicates a deletion. STATE_DELETE 500 // or 'm' Indicates a move. STATE_MOVE 501 if ( state.charAt( 2 ) == 'a' ) 502 { 503 status = ScmFileStatus.ADDED; 504 } 505 else 506 { 507 if ( state.charAt( 2 ) == 'd' ) 508 { 509 status = ScmFileStatus.DELETED; 510 } 511 else 512 { 513 if ( state.charAt( 2 ) == 'm' ) 514 { 515 status = ScmFileStatus.RENAMED; // Has been renamed or moved. 516 } 517 518 // [3] is '-' or 'c' Indicates a content change. STATE_CONTENT_CHANGE 519 if ( state.charAt( 3 ) == 'c' ) 520 { 521 status = ScmFileStatus.MODIFIED; // The file has been modified in the working tree. 522 } 523 524 // [4] is '-' or 'p' Indicates a property change. STATE_PROPERTY_CHANGE 525 if ( state.charAt( 4 ) == 'p' ) 526 { 527 status = 528 ScmFileStatus.MODIFIED; // ScmFileStatus has no concept of property or meta data changes. 529 } 530 } 531 } 532 533 return status; 534 } 535}