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 // changesetDate will be null when the date is not given, it only has just the time. The date is today. 348 if ( changesetDate == null ) 349 { 350 changesetDate = parseDate( date.toString(), userDateFormat, JAZZ_TIMESTAMP_PATTERN_TIME ); 351 // try again forcing en locale 352 if ( changesetDate == null ) 353 { 354 changesetDate = 355 parseDate( date.toString(), userDateFormat, JAZZ_TIMESTAMP_PATTERN_TIME, Locale.ENGLISH ); 356 } 357 // Get today's time/date. Used to get the date. 358 Calendar today = Calendar.getInstance(); 359 // Get a working one. 360 Calendar changesetCal = Calendar.getInstance(); 361 // Set the date/time. Used to set the time. 362 changesetCal.setTimeInMillis( changesetDate.getTime() ); 363 // Now set the date (today). 364 changesetCal.set( today.get( Calendar.YEAR ), today.get( Calendar.MONTH ), 365 today.get( Calendar.DAY_OF_MONTH ) ); 366 // Now get the date of the combined results. 367 changesetDate = changesetCal.getTime(); 368 } 369 370 if ( getLogger().isDebugEnabled() ) 371 { 372 getLogger().debug( " date : " + date ); 373 getLogger().debug( " changesetDate : " + changesetDate ); 374 } 375 376 ChangeSet currentChangeSet = entries.get( currentChangeSetIndex ); 377 currentChangeSet.setDate( changesetDate ); 378 } 379 } 380 381 private void processChangesLine( String line ) 382 { 383 // Process the changes line, eg: 384 // ---c- (1170) \GPDB\GPDBEAR\pom.xml 385 // ---c- (1171) \GPDB\GPDBResources\pom.xml 386 // ---c- (1167) \GPDB\GPDBWeb\pom.xml 387 // ---c- (1165) \GPDB\pom.xml 388 Matcher matcher = CHANGES_PATTERN.matcher( line ); 389 if ( matcher.find() ) 390 { 391 ChangeSet currentChangeSet = entries.get( currentChangeSetIndex ); 392 393 String changeFlags = matcher.group( 1 ); // Not used. 394 String fileAlias = matcher.group( 2 ); 395 String file = matcher.group( 3 ); 396 397 if ( getLogger().isDebugEnabled() ) 398 { 399 getLogger().debug( " Parsing Changes Line : " + line ); 400 getLogger().debug( 401 " changeFlags : " + changeFlags + " Translated to : " + parseFileChangeState( changeFlags ) ); 402 getLogger().debug( " filetAlias : " + fileAlias ); 403 getLogger().debug( " file : " + file ); 404 } 405 406 ChangeFile changeFile = new ChangeFile( file ); 407 ScmFileStatus status = parseFileChangeState( changeFlags ); 408 changeFile.setAction( status ); 409 currentChangeSet.getFiles().add( changeFile ); 410 } 411 } 412 413 /** 414 * String the leading/trailing ", < and > from the text. 415 * 416 * @param text The text to process. 417 * @return The striped text. 418 */ 419 protected String stripDelimiters( String text ) 420 { 421 if ( text == null ) 422 { 423 return null; 424 } 425 String workingText = text; 426 if ( workingText.startsWith( "\"" ) || workingText.startsWith( "<" ) ) 427 { 428 workingText = workingText.substring( 1 ); 429 } 430 if ( workingText.endsWith( "\"" ) || workingText.endsWith( ">" ) ) 431 { 432 workingText = workingText.substring( 0, workingText.length() - 1 ); 433 } 434 435 return workingText; 436 } 437 438 /** 439 * Parse the change state file flags from Jazz and map them to the maven SCM ones. 440 * <p/> 441 * "----" Character positions 0-3. 442 * <p/> 443 * [0] is '*' or '-' Indicates that this is the current change set ('*') or not ('-'). STATE_CHANGESET_CURRENT 444 * [1] is '!' or '-' Indicates a Potential Conflict ('!') or not ('-'). STATE_POTENTIAL_CONFLICT 445 * [2] is '#' or '-' Indicates a Conflict ('#') or not ('-'). STATE_CONFLICT 446 * [3] is '@' or '$' Indicates whether the changeset is active ('@') or not ('$'). STATE_CHANGESET_ACTIVE 447 * 448 * @param state The 5 character long state string 449 * @return The ScmFileStatus value. 450 */ 451 private ScmFileStatus parseChangeSetChangeState( String state ) 452 { 453 if ( state.length() != 4 ) 454 { 455 throw new IllegalArgumentException( "Change State string must be 4 chars long!" ); 456 } 457 458 // This is not used, but is here for potential future usage and for documentation purposes. 459 return ScmFileStatus.UNKNOWN; 460 } 461 462 /** 463 * Parse the change state file flags from Jazz and map them to the maven SCM ones. 464 * <p/> 465 * "-----" Character positions 0-4. The default is '-'. 466 * <p/> 467 * [0] is '-' or '!' Indicates a Potential Conflict. STATE_POTENTIAL_CONFLICT 468 * [1] is '-' or '#' Indicates a Conflict. STATE_CONFLICT 469 * [2] is '-' or 'a' Indicates an addition. STATE_ADD 470 * or 'd' Indicates a deletion. STATE_DELETE 471 * or 'm' Indicates a move. STATE_MOVE 472 * [3] is '-' or 'c' Indicates a content change. STATE_CONTENT_CHANGE 473 * [4] is '-' or 'p' Indicates a property change. STATE_PROPERTY_CHANGE 474 * <p/> 475 * NOTE: [3] and [4] can only be set it [2] is NOT 'a' or 'd'. 476 * 477 * @param state The 5 character long state string 478 * @return The SCMxxx value. 479 */ 480 private ScmFileStatus parseFileChangeState( String state ) 481 { 482 if ( state.length() != 5 ) 483 { 484 throw new IllegalArgumentException( "Change State string must be 5 chars long!" ); 485 } 486 487 // NOTE: We have an impedance mismatch here. The Jazz file change flags represent 488 // many different states. However, we can only return *ONE* ScmFileStatus value, 489 // so we need to be careful as to the precedence that we give to them. 490 491 ScmFileStatus status = ScmFileStatus.UNKNOWN; // Probably not a valid initial default value. 492 493 // [0] is '-' or '!' Indicates a Potential Conflict. STATE_POTENTIAL_CONFLICT 494 if ( state.charAt( 0 ) == '!' ) 495 { 496 status = ScmFileStatus.CONFLICT; 497 } 498 // [1] is '-' or '#' Indicates a Conflict. STATE_CONFLICT 499 if ( state.charAt( 1 ) == '#' ) 500 { 501 status = ScmFileStatus.CONFLICT; 502 } 503 504 // [2] is '-' or 'a' Indicates an addition. STATE_ADD 505 // or 'd' Indicates a deletion. STATE_DELETE 506 // or 'm' Indicates a move. STATE_MOVE 507 if ( state.charAt( 2 ) == 'a' ) 508 { 509 status = ScmFileStatus.ADDED; 510 } 511 else 512 { 513 if ( state.charAt( 2 ) == 'd' ) 514 { 515 status = ScmFileStatus.DELETED; 516 } 517 else 518 { 519 if ( state.charAt( 2 ) == 'm' ) 520 { 521 status = ScmFileStatus.RENAMED; // Has been renamed or moved. 522 } 523 524 // [3] is '-' or 'c' Indicates a content change. STATE_CONTENT_CHANGE 525 if ( state.charAt( 3 ) == 'c' ) 526 { 527 status = ScmFileStatus.MODIFIED; // The file has been modified in the working tree. 528 } 529 530 // [4] is '-' or 'p' Indicates a property change. STATE_PROPERTY_CHANGE 531 if ( state.charAt( 4 ) == 'p' ) 532 { 533 status = 534 ScmFileStatus.MODIFIED; // ScmFileStatus has no concept of property or meta data changes. 535 } 536 } 537 } 538 539 return status; 540 } 541}