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        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}