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