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   * @since 1.6
53   */
54  public class Sandbox
55  {
56      // Our date format
57      public static final SimpleDateFormat RLOG_DATEFORMAT = new SimpleDateFormat( "MMMMM d, yyyy - h:mm:ss a" );
58  
59      // File Separator
60      private String fs = System.getProperty( "file.separator" );
61  
62      // MKS API Session Object
63      private APISession api;
64  
65      // Other sandbox specific class variables
66      private Project siProject;
67  
68      private String sandboxDir;
69  
70      private String cpid;
71  
72      // Flag to indicate the overall add operation was successful
73      private boolean addSuccess;
74  
75      // Flag to indicate the overall check-in operation was successful
76      private boolean ciSuccess;
77  
78      /**
79       * Fixes the default includes/excludes patterns for compatibility with MKS Integrity's 'si viewnonmembers' command
80       *
81       * @param pattern String pattern representing the includes/excludes file/directory list
82       */
83      public static String formatFilePatterns( String pattern )
84      {
85          StringBuilder sb = new StringBuilder();
86          if ( null != pattern && pattern.length() > 0 )
87          {
88              String[] tokens = StringUtils.split( pattern, "," );
89              for ( int i = 0; i < tokens.length; i++ )
90              {
91                  String tkn = tokens[i].trim();
92                  if ( tkn.indexOf( "file:" ) != 0 && tkn.indexOf( "dir:" ) != 0 )
93                  {
94                      sb.append( tkn.indexOf( '.' ) > 0
95                                     ? StringUtils.replaceOnce( tkn, "**/", "file:" )
96                                     : StringUtils.replaceOnce( tkn, "**/", "dir:" ) );
97                  }
98                  else
99                  {
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 }