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