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.Response;
028import com.mks.api.response.WorkItem;
029import com.mks.api.response.WorkItemIterator;
030import com.mks.api.si.SIModelTypeName;
031
032import java.util.ArrayList;
033import java.util.Calendar;
034import java.util.Collections;
035import java.util.Comparator;
036import java.util.Date;
037import java.util.Hashtable;
038import java.util.List;
039import java.util.NoSuchElementException;
040
041/**
042 * This class represents a MKS Integrity Configuration Management Project
043 * <br>Provides metadata information about a Project
044 *
045 * @author <a href="mailto:cletus@mks.com">Cletus D'Souza</a>
046 * @since 1.6
047 */
048public class Project
049{
050    public static final String NORMAL_PROJECT = "Normal";
051
052    public static final String VARIANT_PROJECT = "Variant";
053
054    public static final String BUILD_PROJECT = "Build";
055
056    private String projectName;
057
058    private String projectType;
059
060    private String projectRevision;
061
062    private String fullConfigSyntax;
063
064    private Date lastCheckpoint;
065
066    private APISession api;
067
068    // Create a custom comparator to compare project members
069    public static final Comparator<Member> FILES_ORDER = new Comparator<Member>()
070    {
071        public int compare( Member cmm1, Member cmm2 )
072        {
073            return cmm1.getMemberName().compareToIgnoreCase( cmm2.getMemberName() );
074        }
075    };
076
077    /**
078     * Checks if the given value is a valid MKS Integrity Label.
079     * <br>If it's invalid, this method throws an exception providing the reason as string.
080     *
081     * @param tagName The checkpoint label name
082     * @return the error message, or null if label is valid
083     */
084    public static void validateTag( String tagName )
085        throws Exception
086    {
087        if ( tagName == null || tagName.length() == 0 )
088        {
089            throw new Exception( "The checkpoint label string is empty!" );
090        }
091
092        char ch = tagName.charAt( 0 );
093        if ( !( ( 'A' <= ch && ch <= 'Z' ) || ( 'a' <= ch && ch <= 'z' ) ) )
094        {
095            throw new Exception( "The checkpoint label must start with an alpha character!" );
096        }
097
098        for ( char invalid : "$,.:;/\\@".toCharArray() )
099        {
100            if ( tagName.indexOf( invalid ) >= 0 )
101            {
102                throw new Exception(
103                    "The checkpoint label may cannot contain one of the following characters: $ , . : ; / \\ @" );
104            }
105        }
106    }
107
108    /**
109     * Creates an instance of an Integrity SCM Project
110     *
111     * @param api        MKS API Session object
112     * @param configPath Configuration path for the MKS Integrity SCM Project
113     * @throws APIException
114     */
115    public Project( APISession api, String configPath )
116        throws APIException
117    {
118        // Initialize our local APISession
119        this.api = api;
120        try
121        {
122            // Get the project information for this project
123            Command siProjectInfoCmd = new Command( Command.SI, "projectinfo" );
124            siProjectInfoCmd.addOption( new Option( "project", configPath ) );
125            api.getLogger().info( "Preparing to execute si projectinfo for " + configPath );
126            Response infoRes = api.runCommand( siProjectInfoCmd );
127            // Get the only work item from the response
128            WorkItem wi = infoRes.getWorkItems().next();
129            // Get the metadata information about the project
130            Field pjNameFld = wi.getField( "projectName" );
131            Field pjTypeFld = wi.getField( "projectType" );
132            Field pjCfgPathFld = wi.getField( "fullConfigSyntax" );
133            Field pjChkptFld = wi.getField( "lastCheckpoint" );
134
135            // Convert to our class fields
136            // First obtain the project name field
137            if ( null != pjNameFld && null != pjNameFld.getValueAsString() )
138            {
139                projectName = pjNameFld.getValueAsString();
140            }
141            else
142            {
143                api.getLogger().warn( "Project info did not provide a value for the 'projectName' field!" );
144                projectName = "";
145            }
146            // Next, we'll need to know the project type
147            if ( null != pjTypeFld && null != pjTypeFld.getValueAsString() )
148            {
149                projectType = pjTypeFld.getValueAsString();
150                if ( isBuild() )
151                {
152                    // Next, we'll need to know the current build checkpoint for this configuration
153                    Field pjRevFld = wi.getField( "revision" );
154                    if ( null != pjRevFld && null != pjRevFld.getItem() )
155                    {
156                        projectRevision = pjRevFld.getItem().getId();
157                    }
158                    else
159                    {
160                        projectRevision = "";
161                        api.getLogger().warn( "Project info did not provide a vale for the 'revision' field!" );
162                    }
163                }
164            }
165            else
166            {
167                api.getLogger().warn( "Project info did not provide a value for the 'projectType' field!" );
168                projectType = "";
169            }
170            // Most important is the configuration path
171            if ( null != pjCfgPathFld && null != pjCfgPathFld.getValueAsString() )
172            {
173                fullConfigSyntax = pjCfgPathFld.getValueAsString();
174            }
175            else
176            {
177                api.getLogger().error( "Project info did not provide a value for the 'fullConfigSyntax' field!" );
178                fullConfigSyntax = "";
179            }
180            // Finally, we'll need to store the last checkpoint to figure out differences, etc.
181            if ( null != pjChkptFld && null != pjChkptFld.getDateTime() )
182            {
183                lastCheckpoint = pjChkptFld.getDateTime();
184            }
185            else
186            {
187                api.getLogger().warn( "Project info did not provide a value for the 'lastCheckpoint' field!" );
188                lastCheckpoint = Calendar.getInstance().getTime();
189            }
190        }
191        catch ( NoSuchElementException nsee )
192        {
193            api.getLogger().error( "Project info did not provide a value for field " + nsee.getMessage() );
194        }
195    }
196
197    /**
198     * Returns the project path for this Integrity SCM Project
199     *
200     * @return
201     */
202    public String getProjectName()
203    {
204        return projectName;
205    }
206
207    /**
208     * Returns the project revision for this Integrity SCM Project
209     *
210     * @return
211     */
212    public String getProjectRevision()
213    {
214        return projectRevision;
215    }
216
217    /**
218     * Returns true is this is a Normal Project
219     *
220     * @return
221     */
222    public boolean isNormal()
223    {
224        return projectType.equalsIgnoreCase( NORMAL_PROJECT );
225    }
226
227    /**
228     * Returns true if this is a Variant Project
229     *
230     * @return
231     */
232    public boolean isVariant()
233    {
234        return projectType.equalsIgnoreCase( VARIANT_PROJECT );
235    }
236
237    /**
238     * Returns true if this is a Build Project
239     *
240     * @return
241     */
242    public boolean isBuild()
243    {
244        return projectType.equalsIgnoreCase( BUILD_PROJECT );
245    }
246
247    /**
248     * Returns the Full Configuration Path for this Integrity SCM Project
249     *
250     * @return
251     */
252    public String getConfigurationPath()
253    {
254        return fullConfigSyntax;
255    }
256
257    /**
258     * Returns the date when the last checkpoint was performed on this Project
259     *
260     * @return
261     */
262    public Date getLastCheckpointDate()
263    {
264        return lastCheckpoint;
265    }
266
267    /**
268     * Parses the output from the si viewproject command to get a list of members
269     *
270     * @param workspaceDir The current workspace directory, which is required for an export
271     * @return The list of Member objects for this project
272     * @throws APIException
273     */
274    public List<Member> listFiles( String workspaceDir )
275        throws APIException
276    {
277        // Re-initialize the member list for this project
278        List<Member> memberList = new ArrayList<Member>();
279        // Initialize the project config hash
280        Hashtable<String, String> pjConfigHash = new Hashtable<String, String>();
281        // Add the mapping for this project
282        pjConfigHash.put( projectName, fullConfigSyntax );
283        // Compute the project root directory
284        String projectRoot = projectName.substring( 0, projectName.lastIndexOf( '/' ) );
285
286        // Now, lets parse this project
287        Command siViewProjectCmd = new Command( Command.SI, "viewproject" );
288        siViewProjectCmd.addOption( new Option( "recurse" ) );
289        siViewProjectCmd.addOption( new Option( "project", fullConfigSyntax ) );
290        MultiValue mvFields = new MultiValue( "," );
291        mvFields.add( "name" );
292        mvFields.add( "context" );
293        mvFields.add( "memberrev" );
294        mvFields.add( "membertimestamp" );
295        mvFields.add( "memberdescription" );
296        siViewProjectCmd.addOption( new Option( "fields", mvFields ) );
297        api.getLogger().info( "Preparing to execute si viewproject for " + fullConfigSyntax );
298        Response viewRes = api.runCommand( siViewProjectCmd );
299
300        // Iterate through the list of members returned by the API
301        WorkItemIterator wit = viewRes.getWorkItems();
302        while ( wit.hasNext() )
303        {
304            WorkItem wi = wit.next();
305            if ( wi.getModelType().equals( SIModelTypeName.SI_SUBPROJECT ) )
306            {
307                // Save the configuration path for the current subproject, using the canonical path name
308                pjConfigHash.put( wi.getField( "name" ).getValueAsString(), wi.getId() );
309            }
310            else if ( wi.getModelType().equals( SIModelTypeName.MEMBER ) )
311            {
312                // Figure out this member's parent project's canonical path name
313                String parentProject = wi.getField( "parent" ).getValueAsString();
314                // Instantiate our Integrity CM Member object
315                Member iCMMember = new Member( wi, pjConfigHash.get( parentProject ), projectRoot, workspaceDir );
316                // Add this to the full list of members in this project
317                memberList.add( iCMMember );
318            }
319            else
320            {
321                api.getLogger().warn( "View project output contains an invalid model type: " + wi.getModelType() );
322            }
323        }
324
325        // Sort the files list...
326        Collections.sort( memberList, FILES_ORDER );
327        return memberList;
328    }
329
330    /**
331     * Performs a checkpoint on the Integrity SCM Project
332     *
333     * @param message Checkpoint description
334     * @param tag     Checkpoint label
335     * @return MKS API Response object
336     * @throws APIException
337     */
338    public Response checkpoint( String message, String tag )
339        throws APIException
340    {
341        // Setup the checkpoint command
342        api.getLogger().debug( "Checkpointing project " + fullConfigSyntax + " with label '" + tag + "'" );
343        // Construct the checkpoint command
344        Command siCheckpoint = new Command( Command.SI, "checkpoint" );
345        siCheckpoint.addOption( new Option( "recurse" ) );
346        // Set the project name
347        siCheckpoint.addOption( new Option( "project", fullConfigSyntax ) );
348        // Set the label
349        siCheckpoint.addOption( new Option( "label", tag ) );
350        // Set the description, if specified
351        if ( null != message && message.length() > 0 )
352        {
353            siCheckpoint.addOption( new Option( "description", message ) );
354        }
355        // Run the checkpoint command
356        return api.runCommand( siCheckpoint );
357    }
358
359    /**
360     * Creates a Development Path (project branch) for the MKS Integrity SCM Project
361     *
362     * @param devPath Development Path Name
363     * @return MKS API Response object
364     * @throws APIException
365     */
366    public Response createDevPath( String devPath )
367        throws APIException
368    {
369        // First we need to obtain a checkpoint from the current configuration (normal or variant)
370        String chkpt = projectRevision;
371        if ( !isBuild() )
372        {
373            Response chkptRes = checkpoint( "Pre-checkpoint for development path " + devPath, devPath + " Baseline" );
374            WorkItem wi = chkptRes.getWorkItem( fullConfigSyntax );
375            chkpt = wi.getResult().getField( "resultant" ).getItem().getId();
376        }
377
378        // Now lets setup the create development path command
379        api.getLogger().debug(
380            "Creating development path '" + devPath + "' for project " + projectName + " at revision '" + chkpt + "'" );
381        Command siCreateDevPath = new Command( Command.SI, "createdevpath" );
382        siCreateDevPath.addOption( new Option( "devpath", devPath ) );
383        // Set the project name
384        siCreateDevPath.addOption( new Option( "project", projectName ) );
385        // Set the checkpoint we want to create the development path from
386        siCreateDevPath.addOption( new Option( "projectRevision", chkpt ) );
387        // Run the create development path command
388        return api.runCommand( siCreateDevPath );
389    }
390}
391