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