001package org.apache.maven.scm.provider.integrity; 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 com.mks.api.Command; 023import com.mks.api.MultiValue; 024import com.mks.api.Option; 025import com.mks.api.response.APIException; 026import com.mks.api.response.Field; 027import com.mks.api.response.Item; 028import com.mks.api.response.Response; 029import com.mks.api.response.WorkItem; 030import com.mks.api.response.WorkItemIterator; 031import com.mks.api.si.SIModelTypeName; 032import org.apache.maven.scm.ChangeFile; 033import org.apache.maven.scm.ChangeSet; 034import org.apache.maven.scm.ScmFile; 035import org.apache.maven.scm.ScmFileStatus; 036import org.apache.maven.scm.command.changelog.ChangeLogSet; 037import org.codehaus.plexus.util.StringUtils; 038 039import java.io.File; 040import java.text.SimpleDateFormat; 041import java.util.ArrayList; 042import java.util.Date; 043import java.util.Hashtable; 044import java.util.Iterator; 045import java.util.List; 046 047/** 048 * This class represents an MKS Integrity Sandbox and provides an encapsulation 049 * for executing typical Sandbox operations 050 * 051 * @author <a href="mailto:cletus@mks.com">Cletus D'Souza</a> 052 * @since 1.6 053 */ 054public class Sandbox 055{ 056 // Our date format 057 public static final SimpleDateFormat RLOG_DATEFORMAT = new SimpleDateFormat( "MMMMM d, yyyy - h:mm:ss a" ); 058 059 // File Separator 060 private String fs = System.getProperty( "file.separator" ); 061 062 // MKS API Session Object 063 private APISession api; 064 065 // Other sandbox specific class variables 066 private Project siProject; 067 068 private String sandboxDir; 069 070 private String cpid; 071 072 // Flag to indicate the overall add operation was successful 073 private boolean addSuccess; 074 075 // Flag to indicate the overall check-in operation was successful 076 private boolean ciSuccess; 077 078 /** 079 * Fixes the default includes/excludes patterns for compatibility with MKS Integrity's 'si viewnonmembers' command 080 * 081 * @param pattern String pattern representing the includes/excludes file/directory list 082 */ 083 public static String formatFilePatterns( String pattern ) 084 { 085 StringBuilder sb = new StringBuilder(); 086 if ( null != pattern && pattern.length() > 0 ) 087 { 088 String[] tokens = StringUtils.split( pattern, "," ); 089 for ( int i = 0; i < tokens.length; i++ ) 090 { 091 String tkn = tokens[i].trim(); 092 if ( tkn.indexOf( "file:" ) != 0 && tkn.indexOf( "dir:" ) != 0 ) 093 { 094 sb.append( tkn.indexOf( '.' ) > 0 095 ? StringUtils.replaceOnce( tkn, "**/", "file:" ) 096 : StringUtils.replaceOnce( tkn, "**/", "dir:" ) ); 097 } 098 else 099 { 100 sb.append( tkn ); 101 } 102 sb.append( i < tokens.length ? "," : "" ); 103 } 104 } 105 return sb.toString(); 106 } 107 108 /** 109 * The Sandbox constructor 110 * 111 * @param api MKS API Session object 112 * @param cmProject Project object 113 * @param dir Absolute path to the location for the Sandbox directory 114 */ 115 public Sandbox( APISession api, Project cmProject, String dir ) 116 { 117 siProject = cmProject; 118 sandboxDir = dir; 119 this.api = api; 120 cpid = System.getProperty( "maven.scm.integrity.cpid" ); 121 cpid = ( ( null == cpid || cpid.length() == 0 ) ? ":none" : cpid ); 122 addSuccess = true; 123 ciSuccess = true; 124 } 125 126 /** 127 * Attempts to figure out if the current sandbox already exists and is valid 128 * 129 * @param sandbox The client-side fully qualified path to the sandbox pj 130 * @return true/false depending on whether or not this location has a valid sandbox 131 * @throws APIException 132 */ 133 private boolean isValidSandbox( String sandbox ) 134 throws APIException 135 { 136 Command cmd = new Command( Command.SI, "sandboxinfo" ); 137 cmd.addOption( new Option( "sandbox", sandbox ) ); 138 139 api.getLogger().debug( "Validating existing sandbox: " + sandbox ); 140 Response res = api.runCommand( cmd ); 141 WorkItemIterator wit = res.getWorkItems(); 142 try 143 { 144 WorkItem wi = wit.next(); 145 return wi.getField( "fullConfigSyntax" ).getValueAsString().equalsIgnoreCase( 146 siProject.getConfigurationPath() ); 147 } 148 catch ( APIException aex ) 149 { 150 ExceptionHandler eh = new ExceptionHandler( aex ); 151 api.getLogger().error( "MKS API Exception: " + eh.getMessage() ); 152 api.getLogger().debug( eh.getCommand() + " completed with exit code " + eh.getExitCode() ); 153 return false; 154 } 155 } 156 157 /** 158 * Inspects the MKS API Response object's Item field to determine whether or not a working file delta exists 159 * 160 * @param wfdelta MKS API Response object's Item representing the Working File Delta 161 * @return true if the working file is a delta; false otherwise 162 */ 163 private boolean isDelta( Item wfdelta ) 164 { 165 // Return false if there is no working file 166 return wfdelta.getField( "isDelta" ).getBoolean().booleanValue(); 167 } 168 169 /** 170 * Executes a 'si add' command using the message for the description 171 * 172 * @param memberFile Full path to the new member's location 173 * @param message Description for the new member's archive 174 * @return MKS API Response object 175 * @throws APIException 176 */ 177 private Response add( File memberFile, String message ) 178 throws APIException 179 { 180 // Setup the add command 181 api.getLogger().info( "Adding member: " + memberFile.getAbsolutePath() ); 182 Command siAdd = new Command( Command.SI, "add" ); 183 siAdd.addOption( new Option( "onExistingArchive", "sharearchive" ) ); 184 siAdd.addOption( new Option( "cpid", cpid ) ); 185 if ( null != message && message.length() > 0 ) 186 { 187 siAdd.addOption( new Option( "description", message ) ); 188 } 189 siAdd.addOption( new Option( "cwd", memberFile.getParentFile().getAbsolutePath() ) ); 190 siAdd.addSelection( memberFile.getName() ); 191 return api.runCommand( siAdd ); 192 } 193 194 /** 195 * Executes a 'si ci' command using the relativeName for the member name and message for the description 196 * 197 * @param memberFile Full path to the member's current sandbox location 198 * @param relativeName Relative path from the nearest subproject or project 199 * @param message Description for checking in the new update 200 * @return MKS API Response object 201 * @throws APIException 202 */ 203 private Response checkin( File memberFile, String relativeName, String message ) 204 throws APIException 205 { 206 // Setup the check-in command 207 api.getLogger().info( "Checking in member: " + memberFile.getAbsolutePath() ); 208 Command sici = new Command( Command.SI, "ci" ); 209 sici.addOption( new Option( "cpid", cpid ) ); 210 if ( null != message && message.length() > 0 ) 211 { 212 sici.addOption( new Option( "description", message ) ); 213 } 214 sici.addOption( new Option( "cwd", memberFile.getParentFile().getAbsolutePath() ) ); 215 sici.addSelection( relativeName ); 216 return api.runCommand( sici ); 217 } 218 219 /** 220 * Executes a 'si drop' command using the relativeName for the member name 221 * 222 * @param memberFile Full path to the member's current sandbox location 223 * @param relativeName Relative path from the nearest subproject or project 224 * @return MKS API Response object 225 * @throws APIException 226 */ 227 private Response dropMember( File memberFile, String relativeName ) 228 throws APIException 229 { 230 // Setup the drop command 231 api.getLogger().info( "Dropping member " + memberFile.getAbsolutePath() ); 232 Command siDrop = new Command( Command.SI, "drop" ); 233 siDrop.addOption( new Option( "cwd", memberFile.getParentFile().getAbsolutePath() ) ); 234 siDrop.addOption( new Option( "noconfirm" ) ); 235 siDrop.addOption( new Option( "cpid", cpid ) ); 236 siDrop.addOption( new Option( "delete" ) ); 237 siDrop.addSelection( relativeName ); 238 return api.runCommand( siDrop ); 239 } 240 241 /** 242 * Executes a 'si diff' command to see if the working file has actually changed. Even though the 243 * working file delta might be true, that doesn't always mean the file has actually changed. 244 * 245 * @param memberFile Full path to the member's current sandbox location 246 * @param relativeName Relative path from the nearest subproject or project 247 * @return MKS API Response object 248 */ 249 private boolean hasMemberChanged( File memberFile, String relativeName ) 250 { 251 // Setup the differences command 252 Command siDiff = new Command( Command.SI, "diff" ); 253 siDiff.addOption( new Option( "cwd", memberFile.getParentFile().getAbsolutePath() ) ); 254 siDiff.addSelection( relativeName ); 255 try 256 { 257 // Run the diff command... 258 Response res = api.runCommand( siDiff ); 259 try 260 { 261 // Return the changed flag... 262 return res.getWorkItems().next().getResult().getField( "resultant" ).getItem().getField( 263 "different" ).getBoolean().booleanValue(); 264 } 265 catch ( NullPointerException npe ) 266 { 267 api.getLogger().warn( "Couldn't figure out differences for file: " + memberFile.getAbsolutePath() ); 268 api.getLogger().warn( 269 "Null value found along response object for WorkItem/Result/Field/Item/Field.getBoolean()" ); 270 api.getLogger().warn( "Proceeding with the assumption that the file has changed!" ); 271 } 272 } 273 catch ( APIException aex ) 274 { 275 ExceptionHandler eh = new ExceptionHandler( aex ); 276 api.getLogger().warn( "Couldn't figure out differences for file: " + memberFile.getAbsolutePath() ); 277 api.getLogger().warn( eh.getMessage() ); 278 api.getLogger().warn( "Proceeding with the assumption that the file has changed!" ); 279 api.getLogger().debug( eh.getCommand() + " completed with exit Code " + eh.getExitCode() ); 280 } 281 return true; 282 } 283 284 /** 285 * Returns the full path name to the current Sandbox directory 286 * 287 * @return 288 */ 289 public String getSandboxDir() 290 { 291 return sandboxDir; 292 } 293 294 /** 295 * Executes a 'si lock' command using the relativeName of the file 296 * 297 * @param memberFile Full path to the member's current sandbox location 298 * @param relativeName Relative path from the nearest subproject or project 299 * @return MKS API Response object 300 * @throws APIException 301 */ 302 public Response lock( File memberFile, String relativeName ) 303 throws APIException 304 { 305 // Setup the lock command 306 api.getLogger().debug( "Locking member: " + memberFile.getAbsolutePath() ); 307 Command siLock = new Command( Command.SI, "lock" ); 308 siLock.addOption( new Option( "revision", ":member" ) ); 309 siLock.addOption( new Option( "cpid", cpid ) ); 310 siLock.addOption( new Option( "cwd", memberFile.getParentFile().getAbsolutePath() ) ); 311 siLock.addSelection( relativeName ); 312 // Execute the lock command 313 return api.runCommand( siLock ); 314 } 315 316 /** 317 * Executes a 'si unlock' command using the relativeName of the file 318 * 319 * @param memberFile Full path to the member's current sandbox location 320 * @param relativeName Relative path from the nearest subproject or project 321 * @return MKS API Response object 322 * @throws APIException 323 */ 324 public Response unlock( File memberFile, String relativeName ) 325 throws APIException 326 { 327 // Setup the unlock command 328 api.getLogger().debug( "Unlocking member: " + memberFile.getAbsolutePath() ); 329 Command siUnlock = new Command( Command.SI, "unlock" ); 330 siUnlock.addOption( new Option( "revision", ":member" ) ); 331 siUnlock.addOption( new Option( "action", "remove" ) ); 332 siUnlock.addOption( new Option( "cwd", memberFile.getParentFile().getAbsolutePath() ) ); 333 siUnlock.addSelection( relativeName ); 334 // Execute the unlock command 335 return api.runCommand( siUnlock ); 336 } 337 338 /** 339 * Removes the registration for the Sandbox in the user's profile 340 * 341 * @return The API Response associated with executing this command 342 * @throws APIException 343 */ 344 public Response drop() 345 throws APIException 346 { 347 File project = new File( siProject.getProjectName() ); 348 File sandboxpj = new File( sandboxDir + fs + project.getName() ); 349 350 // Check to see if the sandbox file already exists and its OK to use 351 api.getLogger().debug( "Sandbox Project File: " + sandboxpj.getAbsolutePath() ); 352 Command cmd = new Command( Command.SI, "dropsandbox" ); 353 cmd.addOption( new Option( "delete", "members" ) ); 354 cmd.addOption( new Option( "sandbox", sandboxpj.getAbsolutePath() ) ); 355 cmd.addOption( new Option( "cwd", sandboxDir ) ); 356 return api.runCommand( cmd ); 357 } 358 359 /** 360 * Creates a new Sandbox in the sandboxDir specified 361 * 362 * @return true if the operation is successful; false otherwise 363 * @throws APIException 364 */ 365 public boolean create() 366 throws APIException 367 { 368 File project = new File( siProject.getProjectName() ); 369 File sandboxpj = new File( sandboxDir + fs + project.getName() ); 370 371 // Check to see if the sandbox file already exists and its OK to use 372 api.getLogger().debug( "Sandbox Project File: " + sandboxpj.getAbsolutePath() ); 373 if ( sandboxpj.isFile() ) 374 { 375 // Validate this sandbox 376 if ( isValidSandbox( sandboxpj.getAbsolutePath() ) ) 377 { 378 api.getLogger().debug( 379 "Reusing existing Sandbox in " + sandboxDir + " for project " + siProject.getConfigurationPath() ); 380 return true; 381 } 382 else 383 { 384 api.getLogger().error( 385 "An invalid Sandbox exists in " + sandboxDir + ". Please provide a different location!" ); 386 return false; 387 } 388 } 389 else // Create a new sandbox in the location specified 390 { 391 api.getLogger().debug( 392 "Creating Sandbox in " + sandboxDir + " for project " + siProject.getConfigurationPath() ); 393 try 394 { 395 Command cmd = new Command( Command.SI, "createsandbox" ); 396 cmd.addOption( new Option( "recurse" ) ); 397 cmd.addOption( new Option( "nopopulate" ) ); 398 cmd.addOption( new Option( "project", siProject.getConfigurationPath() ) ); 399 cmd.addOption( new Option( "cwd", sandboxDir ) ); 400 api.runCommand( cmd ); 401 } 402 catch ( APIException aex ) 403 { 404 // Check to see if this exception is due an existing sandbox registry entry 405 ExceptionHandler eh = new ExceptionHandler( aex ); 406 if ( eh.getMessage().indexOf( "There is already a registered entry" ) > 0 ) 407 { 408 // This will re-validate the sandbox, if Maven blew away the old directory 409 return create(); 410 } 411 else 412 { 413 throw aex; 414 } 415 } 416 return true; 417 } 418 } 419 420 /** 421 * Resynchronizes an existing Sandbox 422 * Assumes that the create() call has already been made to validate this sandbox 423 * 424 * @throws APIException 425 */ 426 public Response resync() 427 throws APIException 428 { 429 api.getLogger().debug( 430 "Resynchronizing Sandbox in " + sandboxDir + " for project " + siProject.getConfigurationPath() ); 431 Command cmd = new Command( Command.SI, "resync" ); 432 cmd.addOption( new Option( "recurse" ) ); 433 cmd.addOption( new Option( "populate" ) ); 434 cmd.addOption( new Option( "cwd", sandboxDir ) ); 435 return api.runCommand( cmd ); 436 } 437 438 /** 439 * Executes a 'si makewritable' command to allow edits to all files in the Sandbox directory 440 * 441 * @return MKS API Response object 442 * @throws APIException 443 */ 444 public Response makeWriteable() 445 throws APIException 446 { 447 api.getLogger().debug( 448 "Setting files to writeable in " + sandboxDir + " for project " + siProject.getConfigurationPath() ); 449 Command cmd = new Command( Command.SI, "makewritable" ); 450 cmd.addOption( new Option( "recurse" ) ); 451 cmd.addOption( new Option( "cwd", sandboxDir ) ); 452 return api.runCommand( cmd ); 453 } 454 455 /** 456 * Executes a 'si revert' command to roll back changes to all files in the Sandbox directory 457 * 458 * @return MKS API Response object 459 * @throws APIException 460 */ 461 public Response revertMembers() 462 throws APIException 463 { 464 api.getLogger().debug( 465 "Reverting changes in sandbox " + sandboxDir + " for project " + siProject.getConfigurationPath() ); 466 Command cmd = new Command( Command.SI, "revert" ); 467 cmd.addOption( new Option( "recurse" ) ); 468 cmd.addOption( new Option( "cwd", sandboxDir ) ); 469 return api.runCommand( cmd ); 470 } 471 472 /** 473 * Executes a 'si viewnonmembers' command filtering the results using the exclude and include lists 474 * 475 * @param exclude Pattern containing the exclude file list 476 * @param include Pattern containing the include file list 477 * @return List of ScmFile objects representing the new files in the Sandbox 478 * @throws APIException 479 */ 480 public List<ScmFile> getNewMembers( String exclude, String include ) 481 throws APIException 482 { 483 // Store a list of files that were added to the repository 484 List<ScmFile> filesAdded = new ArrayList<ScmFile>(); 485 Command siViewNonMem = new Command( Command.SI, "viewnonmembers" ); 486 siViewNonMem.addOption( new Option( "recurse" ) ); 487 if ( null != exclude && exclude.length() > 0 ) 488 { 489 siViewNonMem.addOption( new Option( "exclude", exclude ) ); 490 } 491 if ( null != include && include.length() > 0 ) 492 { 493 siViewNonMem.addOption( new Option( "include", include ) ); 494 } 495 siViewNonMem.addOption( new Option( "noincludeFormers" ) ); 496 siViewNonMem.addOption( new Option( "cwd", sandboxDir ) ); 497 Response response = api.runCommand( siViewNonMem ); 498 for ( WorkItemIterator wit = response.getWorkItems(); wit.hasNext(); ) 499 { 500 filesAdded.add( 501 new ScmFile( wit.next().getField( "absolutepath" ).getValueAsString(), ScmFileStatus.ADDED ) ); 502 } 503 return filesAdded; 504 505 } 506 507 /** 508 * Adds a list of files to the MKS Integrity SCM Project 509 * 510 * @param exclude Pattern containing the exclude file list 511 * @param include Pattern containing the include file list 512 * @param message Description for the member's archive 513 * @return 514 */ 515 public List<ScmFile> addNonMembers( String exclude, String include, String message ) 516 { 517 // Re-initialize the overall addSuccess to be true for now 518 addSuccess = true; 519 // Store a list of files that were actually added to the repository 520 List<ScmFile> filesAdded = new ArrayList<ScmFile>(); 521 api.getLogger().debug( "Looking for new members in sandbox dir: " + sandboxDir ); 522 try 523 { 524 List<ScmFile> newFileList = getNewMembers( exclude, include ); 525 for ( Iterator<ScmFile> sit = newFileList.iterator(); sit.hasNext(); ) 526 { 527 try 528 { 529 ScmFile localFile = sit.next(); 530 // Attempt to add the file to the Integrity repository 531 add( new File( localFile.getPath() ), message ); 532 // If it was a success, then add it to the list of files that were actually added 533 filesAdded.add( localFile ); 534 } 535 catch ( APIException aex ) 536 { 537 // Set the addSuccess to false, since we ran into a problem 538 addSuccess = false; 539 ExceptionHandler eh = new ExceptionHandler( aex ); 540 api.getLogger().error( "MKS API Exception: " + eh.getMessage() ); 541 api.getLogger().debug( eh.getCommand() + " completed with exit Code " + eh.getExitCode() ); 542 } 543 } 544 } 545 catch ( APIException aex ) 546 { 547 // Set the addSuccess to false, since we ran into a problem 548 addSuccess = false; 549 ExceptionHandler eh = new ExceptionHandler( aex ); 550 api.getLogger().error( "MKS API Exception: " + eh.getMessage() ); 551 api.getLogger().debug( eh.getCommand() + " completed with exit Code " + eh.getExitCode() ); 552 } 553 return filesAdded; 554 } 555 556 /** 557 * Returns the overall success of the add operation 558 * 559 * @return 560 */ 561 public boolean getOverallAddSuccess() 562 { 563 return addSuccess; 564 } 565 566 /** 567 * Inspects the MKS API Response object's Item field to determine whether or nor a working file exists 568 * 569 * @param wfdelta MKS API Response object's Item representing the Working File Delta 570 * @return 571 */ 572 public boolean hasWorkingFile( Item wfdelta ) 573 { 574 // Return false if there is no working file 575 return !wfdelta.getField( "noWorkingFile" ).getBoolean().booleanValue(); 576 } 577 578 /** 579 * Executes a 'si viewsandbox' and parses the output for changed or dropped working files 580 * 581 * @return A list of MKS API Response WorkItem objects representing the changes in the Sandbox 582 * @throws APIException 583 */ 584 public List<WorkItem> getChangeList() 585 throws APIException 586 { 587 // Store a list of files that were changed/removed to the repository 588 List<WorkItem> changedFiles = new ArrayList<WorkItem>(); 589 // Setup the view sandbox command to figure out what has changed... 590 Command siViewSandbox = new Command( Command.SI, "viewsandbox" ); 591 // Create the --fields option 592 MultiValue mv = new MultiValue( "," ); 593 mv.add( "name" ); 594 mv.add( "context" ); 595 mv.add( "wfdelta" ); 596 mv.add( "memberarchive" ); 597 siViewSandbox.addOption( new Option( "fields", mv ) ); 598 siViewSandbox.addOption( new Option( "recurse" ) ); 599 siViewSandbox.addOption( new Option( "noincludeDropped" ) ); 600 siViewSandbox.addOption( new Option( "filterSubs" ) ); 601 siViewSandbox.addOption( new Option( "cwd", sandboxDir ) ); 602 603 // Run the view sandbox command 604 Response r = api.runCommand( siViewSandbox ); 605 // Check-in all changed files, drop all members with missing working files 606 for ( WorkItemIterator wit = r.getWorkItems(); wit.hasNext(); ) 607 { 608 WorkItem wi = wit.next(); 609 api.getLogger().debug( "Inspecting file: " + wi.getField( "name" ).getValueAsString() ); 610 611 if ( wi.getModelType().equals( SIModelTypeName.MEMBER ) ) 612 { 613 Item wfdeltaItem = (Item) wi.getField( "wfdelta" ).getValue(); 614 // Proceed with this entry only if it is an actual working file delta 615 if ( isDelta( wfdeltaItem ) ) 616 { 617 File memberFile = new File( wi.getField( "name" ).getValueAsString() ); 618 if ( hasWorkingFile( wfdeltaItem ) ) 619 { 620 // Only report on files that have actually changed... 621 if ( hasMemberChanged( memberFile, wi.getId() ) ) 622 { 623 changedFiles.add( wi ); 624 } 625 } 626 else 627 { 628 // Also report on dropped files 629 changedFiles.add( wi ); 630 } 631 } 632 } 633 } 634 return changedFiles; 635 } 636 637 /** 638 * Wrapper function to check-in all changes and drop members associated with missing working files 639 * 640 * @param message Description for the changes 641 * @return 642 */ 643 public List<ScmFile> checkInUpdates( String message ) 644 { 645 // Re-initialize the overall ciSuccess to be true for now 646 ciSuccess = true; 647 // Store a list of files that were changed/removed to the repository 648 List<ScmFile> changedFiles = new ArrayList<ScmFile>(); 649 api.getLogger().debug( "Looking for changed and dropped members in sandbox dir: " + sandboxDir ); 650 651 try 652 { 653 // Let the list of changed files 654 List<WorkItem> changeList = getChangeList(); 655 // Check-in all changed files, drop all members with missing working files 656 for ( Iterator<WorkItem> wit = changeList.iterator(); wit.hasNext(); ) 657 { 658 try 659 { 660 WorkItem wi = wit.next(); 661 File memberFile = new File( wi.getField( "name" ).getValueAsString() ); 662 // Check-in files that have actually changed... 663 if ( hasWorkingFile( (Item) wi.getField( "wfdelta" ).getValue() ) ) 664 { 665 // Lock each member as you go... 666 lock( memberFile, wi.getId() ); 667 // Commit the changes... 668 checkin( memberFile, wi.getId(), message ); 669 // Update the changed file list 670 changedFiles.add( new ScmFile( memberFile.getAbsolutePath(), ScmFileStatus.CHECKED_IN ) ); 671 } 672 else 673 { 674 // Drop the member if there is no working file 675 dropMember( memberFile, wi.getId() ); 676 // Update the changed file list 677 changedFiles.add( new ScmFile( memberFile.getAbsolutePath(), ScmFileStatus.DELETED ) ); 678 } 679 } 680 catch ( APIException aex ) 681 { 682 // Set the ciSuccess to false, since we ran into a problem 683 ciSuccess = false; 684 ExceptionHandler eh = new ExceptionHandler( aex ); 685 api.getLogger().error( "MKS API Exception: " + eh.getMessage() ); 686 api.getLogger().debug( eh.getCommand() + " completed with exit Code " + eh.getExitCode() ); 687 } 688 } 689 } 690 catch ( APIException aex ) 691 { 692 // Set the ciSuccess to false, since we ran into a problem 693 ciSuccess = false; 694 ExceptionHandler eh = new ExceptionHandler( aex ); 695 api.getLogger().error( "MKS API Exception: " + eh.getMessage() ); 696 api.getLogger().debug( eh.getCommand() + " completed with exit Code " + eh.getExitCode() ); 697 } 698 699 return changedFiles; 700 } 701 702 /** 703 * Returns the overall success of the check-in operation 704 * 705 * @return 706 */ 707 public boolean getOverallCheckInSuccess() 708 { 709 return ciSuccess; 710 } 711 712 /** 713 * Creates one subproject per directory, as required. 714 * 715 * @param dirPath A relative path structure of folders 716 * @return Response containing the result for the created subproject 717 * @throws APIException 718 */ 719 public Response createSubproject( String dirPath ) 720 throws APIException 721 { 722 // Setup the create subproject command 723 api.getLogger().debug( "Creating subprojects for: " + dirPath + "/project.pj" ); 724 Command siCreateSubproject = new Command( Command.SI, "createsubproject" ); 725 siCreateSubproject.addOption( new Option( "cpid", cpid ) ); 726 siCreateSubproject.addOption( new Option( "createSubprojects" ) ); 727 siCreateSubproject.addOption( new Option( "cwd", sandboxDir ) ); 728 siCreateSubproject.addSelection( dirPath + "/project.pj" ); 729 // Execute the create subproject command 730 return api.runCommand( siCreateSubproject ); 731 } 732 733 /** 734 * Executes the 'si rlog' command to generate a list of changed revision found between startDate and endDate 735 * 736 * @param startDate The date range for the beginning of the operation 737 * @param endDate The date range for the end of the operation 738 * @return ChangeLogSet containing a list of changes grouped by Change Package ID 739 * @throws APIException 740 */ 741 public ChangeLogSet getChangeLog( Date startDate, Date endDate ) 742 throws APIException 743 { 744 // Initialize our return object 745 ChangeLogSet changeLog = new ChangeLogSet( startDate, endDate ); 746 // By default we're going to group-by change package 747 // Non change package changes will be lumped into one big Change Set 748 Hashtable<String, ChangeSet> changeSetHash = new Hashtable<String, ChangeSet>(); 749 750 // Lets prepare our si rlog command for execution 751 Command siRlog = new Command( Command.SI, "rlog" ); 752 siRlog.addOption( new Option( "recurse" ) ); 753 MultiValue rFilter = new MultiValue( ":" ); 754 rFilter.add( "daterange" ); 755 rFilter.add( "'" + RLOG_DATEFORMAT.format( startDate ) + "'-'" + RLOG_DATEFORMAT.format( endDate ) + "'" ); 756 siRlog.addOption( new Option( "rfilter", rFilter ) ); 757 siRlog.addOption( new Option( "cwd", sandboxDir ) ); 758 // Execute the si rlog command 759 Response response = api.runCommand( siRlog ); 760 for ( WorkItemIterator wit = response.getWorkItems(); wit.hasNext(); ) 761 { 762 WorkItem wi = wit.next(); 763 String memberName = wi.getContext(); 764 // We're going to have to do a little dance to get the correct server file name 765 memberName = memberName.substring( 0, memberName.lastIndexOf( '/' ) ); 766 memberName = memberName + '/' + wi.getId(); 767 memberName = memberName.replace( '\\', '/' ); 768 // Now lets get the revisions for this file 769 Field revisionsFld = wi.getField( "revisions" ); 770 if ( null != revisionsFld && revisionsFld.getDataType().equals( Field.ITEM_LIST_TYPE ) 771 && null != revisionsFld.getList() ) 772 { 773 @SuppressWarnings( "unchecked" ) List<Item> revList = revisionsFld.getList(); 774 for ( Iterator<Item> lit = revList.iterator(); lit.hasNext(); ) 775 { 776 Item revisionItem = lit.next(); 777 String revision = revisionItem.getId(); 778 String author = revisionItem.getField( "author" ).getItem().getId(); 779 // Attempt to get the full name, if available 780 try 781 { 782 author = revisionItem.getField( "author" ).getItem().getField( "fullname" ).getValueAsString(); 783 } 784 catch ( NullPointerException npe ) 785 { /* ignore */ } 786 String cpid = ":none"; 787 // Attempt to get the cpid for this revision 788 try 789 { 790 cpid = revisionItem.getField( "cpid" ).getItem().getId(); 791 } 792 catch ( NullPointerException npe ) 793 { /* ignore */ } 794 // Get the Change Package summary for this revision 795 String comment = cpid + ": " + revisionItem.getField( "cpsummary" ).getValueAsString(); 796 // Get the date associated with this revision 797 Date date = revisionItem.getField( "date" ).getDateTime(); 798 799 // Lets create our ChangeFile based on the information we've gathered so far 800 ChangeFile changeFile = new ChangeFile( memberName, revision ); 801 802 // Check to see if we already have a ChangeSet grouping for this revision 803 ChangeSet changeSet = changeSetHash.get( cpid ); 804 if ( null != changeSet ) 805 { 806 // Set the date of the ChangeSet to the oldest entry 807 if ( changeSet.getDate().after( date ) ) 808 { 809 changeSet.setDate( date ); 810 } 811 // Add the new ChangeFile 812 changeSet.addFile( changeFile ); 813 // Update the changeSetHash 814 changeSetHash.put( cpid, changeSet ); 815 } 816 else // Create a new ChangeSet grouping and add the ChangeFile 817 { 818 List<ChangeFile> changeFileList = new ArrayList<ChangeFile>(); 819 changeFileList.add( changeFile ); 820 changeSet = new ChangeSet( date, comment, author, changeFileList ); 821 // Update the changeSetHash with an initial entry for the cpid 822 changeSetHash.put( cpid, changeSet ); 823 } 824 } 825 } 826 827 } 828 829 // Update the Change Log with the Change Sets 830 List<ChangeSet> changeSetList = new ArrayList<ChangeSet>(); 831 changeSetList.addAll( changeSetHash.values() ); 832 changeLog.setChangeSets( changeSetList ); 833 834 return changeLog; 835 } 836}