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