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