Unique Files Names SCM Advisor

In (specially) mainframe development environment, there are situations in which duplicate element names can be problematic. And from time to time I get helping customers migrate to Rational Team Concert, I find them asking of a way of ensuring this source elements names uniqueness in SCM can be enforced. In this post I am going to show an advisor for such enforcement, and talk about some restrictions to the approach.

In this post I am going to publish code, so our lawyers reminded me to state that the code in this post is derived from examples from Jazz.net as well as the RTC SDK. The usage of code from that example source code is governed by this license. Therefore this code is governed by this license. Please also remember, as stated in the disclaimer, that this code comes with lack of promise or guarantee. At the end of the post you will find the complete code solution to be downloaded.

Before you read further, in this post I am going to publish and discuss several chunks of code. If you are merely interested in the use case and solution itself, you can read the first overview sections and then jump right to the end to the Conclusions one.

Scenario Overview

The use case to cover is quite simple in its explanation, while hiding complexity in the implementation. We could word it as “I want the developers in my team to get restricted to create new programs in SCM with the same name as existing ones“. From that simple statement, several questions arise:

  • When to restrict? At what time is good to check, or too early/late?
  • How to notify the developer?
  • Any constraints?

I personally think in this solution, starting from the constraints helps in clearing the view for a potential approach, so I will review them next.

Constraints

The very first constraint to came to my mind was given the fact that customers wanted to check as soon as possible. Customers usually think of the checkin operation, but is something we don’t currently have in RTC (see allow process advisors on checkin). Therefore, the next obvious place to think where to place the control is the deliver operation.

Next let’s talk about the scope of the check. Normally when you dare to ask such a question, people usually answer “all my code”. Well, that is a typical answer of trying to shield everything so I make sure I don’t miss an important case, however you have to consider:

  1. There is no easy way to review the whole repository checking for names match
  2. Even in that case, what would be the performance for the user in a potentially huge code repository?
  3. We are trying to avoid having problems in our runtime because of collisions. So we obviously can think of real scope of whatever integration levels are to be defined.

Solution Overview

Taking in consideration the constraints and concerns explained, the solution is based on: on deliver operation, check any files additions/renames, and review a reference stream to check if such an element with a same name already exists. I acknowledge that the reference stream, depending on situations and needs, can be of two forms: (1) to check only if it is the target of my current deliver; (2) to check every time, considering it my code base “reference”. I tried to make the advisor flexible enough to cover both options.

In the rest of the post I won’t review the complete implementation, but rather I will focus on particular steps of it that I believe are of special interest. At the end of this post you can download the complete sample and review it in detail.

Implementation: Process Configuration

I like to start this type of advisors by figuring out what is the process configuration for them and build the code around it, and the expected operations. In this case, I think of a XML fragment like the following:

<streams onlyontarget="false" ignorefileext="true">
        <stream name="name" uuid="XXXXXX"/>
 </streams>

The “onlyontarget” boolean parameter will determine whether to check the streams only if they are target of the current deliver. While the “ignorefileext” will mean if to check files without the file extension or considering it.

From a simple XML fragment as that, we can generate a XSD Schema like the following to use for the advisor following https://jazz.net/wiki/bin/view/Main/ProcessExtensionSchemas#Precondition_Template:

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns="http://com.ibm.ull.scm/uniqueFileNames"
    targetNamespace="http://com.ibm.ull.scm/uniqueFileNames"
    xmlns:process="http://com.ibm.team.process"
    attributeFormDefault="unqualified" elementFormDefault="qualified">
    
    <xsd:import namespace="http://com.ibm.team.process" 
       schemaLocation="platform:/plugin/com.ibm.team.process.common/schema/ProcessSettings.xsd"/>
    
    <xsd:complexType name="uniqueFileNamesType">
      <xsd:complexContent>
        <xsd:restriction base="process:preconditionType">
          <xsd:sequence>
            <xsd:element name="streams" maxOccurs="1" minOccurs="0">
              <xsd:complexType>
                <xsd:sequence>
                  <xsd:element name="stream" maxOccurs="unbounded" minOccurs="0">
                    <xsd:complexType>
                      <xsd:attribute name="name" type="xsd:string" use="required"/>
                      <xsd:attribute name="uuid" type="xsd:string" use="required"/>
                    </xsd:complexType>
                  </xsd:element>
                </xsd:sequence>
                <xsd:attribute name="onlyontarget" type="xsd:boolean" use="required"/>
                <xsd:attribute name="ignorefileext" type="xsd:boolean" use="required"/>
              </xsd:complexType>
            </xsd:element>
          </xsd:sequence>
          <xsd:attribute name="id" type="xsd:string" use="required" fixed="com.ibm.ull.scm.uniqueFileNames"/>
        </xsd:restriction>
      </xsd:complexContent>
  </xsd:complexType>

  <xsd:element name="precondition" substitutionGroup="process:precondition" type="uniqueFileNamesType"/>
</xsd:schema>

Implementation: Reading Process Configuration

Once we have defined the configuration, the first step that the advisor will do is try to read it, and get the relevant pieces of data from it:

/*
* 
* Check if the operation data is the one expected for in Phase2 of the Deliver
* We should get a IScmDeltaSource instance data
* 
*/
    
Object data = operation.getOperationData();
if(!(data instanceof IScmDeltaSource))
{
  collector.addInfo(collector.
    createProblemInfo(Messages.UniqueFileNamesAdvisor_0, 
    Messages.UniqueFileNamesAdvisor_1, ID_CONFIGURATION_PROBLEM));
  monitor.done();
  return;
}
              
IScmDeltaSource deliverData = (IScmDeltaSource) data;
         
/*
* 
* Check that we are in the middle of a server deliver operation
* 
*/
        
if (! deliverData.matchesOperationType(IScmProcessConfiguration.DELIVER_SERVER_OPERATION_ID))
{
  collector.addInfo(collector.
    createProblemInfo(Messages.UniqueFileNamesAdvisor_0, 
    Messages.UniqueFileNamesAdvisor_1, ID_CONFIGURATION_PROBLEM));
  monitor.done();
  return;
}
             
// Check if configuration has been specified
if(advisorConfiguration.getChildren().length == 0)
{
  collector.addInfo(collector.
    createProblemInfo(Messages.UniqueFileNamesAdvisor_0, 
    Messages.UniqueFileNamesAdvisor_1, ID_CONFIGURATION_PROBLEM));
  monitor.done();
  return;
}
        
// Initialize the configuration vbles;
Set<UUID> uniqCtrlStreamsByUUID = null;
boolean onlyIftarget = false;
boolean ignoreExt = false;
        
// If we do have a configuration, get it from process
if (advisorConfiguration.getChildren()[0].getName().
     equals(IUniqueNamesProcessDefinitions.TAG_STREAMS))
{     
  uniqCtrlStreamsByUUID = getStreamsFromConfig(advisorConfiguration.
     getChildren()[0], operation.getProcessArea().getProjectArea()); 
  onlyIftarget = Boolean.valueOf(advisorConfiguration.getChildren()[0].
     getAttribute(IUniqueNamesProcessDefinitions.ONLY_TARGET));
  ignoreExt = Boolean.valueOf(advisorConfiguration.getChildren()[0].
     getAttribute(IUniqueNamesProcessDefinitions.IGNORE_EXT));
}

if(uniqCtrlStreamsByUUID == null || uniqCtrlStreamsByUUID.isEmpty())
{
  // Throwing error and stopping execution. We may want to remove this and keep a simple logging and return without error.
  collector.addInfo(collector.
    createProblemInfo(Messages.UniqueFileNamesAdvisor_2, 
    Messages.UniqueFileNamesAdvisor_3, ID_CONFIGURATION_PROBLEM));
  monitor.done();
  return;
}

If the configuration is wrong I throw an error. This is usually good for testing purposes, but mostly too much for a real working system. You might want to just log an exception and keep the operation running.

By the way, the interface that is reference in the code basically holds the constants that the code needs to know about to handle the process. Something like:

    public static final String STREAM_NAME = "name"; //$NON-NLS-1$
    public static final String STREAM_UUID = "uuid"; //$NON-NLS-1$
    public static final String TAG_STREAM = "stream"; //$NON-NLS-1$
    public static final String ONLY_TARGET = "onlyontarget"; //$NON-NLS-1$
    public static final String IGNORE_EXT = "ignorefileext"; //$NON-NLS-1$
    public static final String TAG_STREAMS = "streams"; //$NON-NLS-1$

 Implementation: Gathering Changes to Check

Now we have identified the streams that are configured, and whether we should check them always, or just as part of a certain deliver, we want to gather all the changes that are relevant in that context, if any. In this case, I want the changes to be organized per the target of the deliver, to easily discriminate them when needed:

Map<UUID, List<IVersionableHandle>> changesToCheck = new HashMap<UUID, List<IVersionableHandle>>();
         
for (IChangeHistoryAddOperandDelta delta : deliverData.getDeltas(IChangeHistoryAddOperandDelta.class)) 
{
  List<IVersionableHandle> deltaChanges = getChangesFromDelta(delta);
  if(deltaChanges != null && !deltaChanges.isEmpty())
  {
    if(onlyIftarget) // in this case we need to check if our target stream is a "guarded" one
    {
       if(uniqCtrlStreamsByUUID.contains(delta.getTargetWorkspace().getItemId()))
       {
          changesToCheck.put(delta.getTargetWorkspace().getItemId(), deltaChanges);
       }
     }
     else
     {
        changesToCheck.put(delta.getTargetWorkspace().getItemId(), deltaChanges);
     }
 }        
}

From the previous snippet, the real interesting method is the following, where the changes are filtered so only those that have to do with naming modifications are considered: new elements creation and elements rename:

private List<IVersionableHandle> getChangesFromDelta(IChangeHistoryAddOperandDelta delta) throws TeamRepositoryException 
{
  List<IChangeSetHandle> changesH = new ArrayList<IChangeSetHandle>();
  List<IVersionableHandle> resultChanges = new ArrayList<IVersionableHandle>();
        
  for (IComponentHandle comp : delta.getComponents()) 
  {
    for (IChangeSetHandle cs : delta.getAdditionalBaselineChangeSets(comp))
    {
      changesH.add(cs);
    }

    for (IChangeSetHandle cs : delta.getAdditionalChangeSets(comp)) {
      changesH.add(cs);
    }
 }
        
 if(changesH.size() >0)
 {
    IItem[] changeSetItems = 
      getItemServer().fetchItems(changesH.toArray(new IItemHandle[changesH.size()]), IRepositoryItemService.COMPLETE);
    for (IItem item : changeSetItems) 
    {
       IChangeSet cs = (IChangeSet) item;
                
       List<IChange> changes = cs.changes();                
       for (IChange change: changes)
       {    
          if((change.kind() == IChange.ADD || change.kind() == IChange.RENAME) 
           && (change.afterState() instanceof IFileItemHandle))
          {
            resultChanges.add(change.afterState());
          }
       }
 }
}
        
return resultChanges;
}

Implementation: Finding and Reporting Duplicates

The following is a method that, for each stream to check, looks for a file matching the criteria.

private void checkForStream(IAdvisorInfoCollector collector, 
List<IVersionableHandle> toCheck, IWorkspace streamContext, 
boolean ignoreExtension, IProgressMonitor monitor) throws TeamRepositoryException
{
   IComponentHandle[] comps = getScmServer().getComponentsForWorkspace(streamContext, null);
        
   IVersionable[] versionsToVerify = 
      getScmServer().fetchStates(toCheck.toArray(new IVersionableHandle[toCheck.size()]), IScmService.COMPLETE, getRepoMonitor(monitor));
        
   for (IVersionable versionToVerify : versionsToVerify) 
   {
            
     List<IVersionableHandle> matchFound = 
       new ArrayList<IVersionableHandle>();
            
     for (IComponentHandle compHandle : comps) 
     {
       ServiceConfigurationProvider configProvider = 
         createConfigProvider(streamContext,compHandle);
                
       String fileNameToCheck = versionToVerify.getName();
       fileNameToCheck = (fileNameToCheck.contains(".") && ignoreExtension)?
           fileNameToCheck.substring(0, fileNameToCheck.lastIndexOf('.')):fileNameToCheck; //$NON-NLS-1$
                
       IAncestorReport[] report = 
        getScmServer().configurationFindByName(configProvider,fileNameToCheck, null, getRepoMonitor(monitor));
                
       if(report.length > 0)
       {
         for (IAncestorReport reportEntry : report) 
         {                        
            List pairs = reportEntry.getNameItemPairs();                
                        
            for (Object pair : pairs) 
            {
               if(pair instanceof INameItemPair)
               {
                 IVersionableHandle handle = ((INameItemPair)pair).getItem();
                 if(handle instanceof IFileItemHandle)
                    matchFound.add(handle);
               }
            }
         }

      }

   }
            
   if(matchFound.size() > 0)
   {
       IAdvisorInfo info = 
          collector.createProblemInfo(Messages.UniqueFileNamesAdvisor_5, 
          NLS.bind(Messages.UniqueFileNamesAdvisor_6, 
          new Object[] {matchFound.size(), versionToVerify.getName(), streamContext.getName()}), ID_DUPLICATE_NAMES_PROBLEM);
       collector.addInfo(info);
   }
                
 }
}

Note that an important aspect of the previous method is the “IScmService.configurationFindByName” used to look for items with a certain name. It receives as parameter a configuration provider created by the stream we are inspecting, and, in each iteration, one of the held components.

Finally, if matches are found, the problem is reported. I have picked a personal way of reporting the matches, where I sum all of potential matches and report the final count; for situations in which the advisor has been configured a time where duplicates already existed (to avoid several errors). But you may want to modify the code and report the information on other way more useful to you.

 Conclusion

This has resulted in a rather long post. I hope you find the solution interesting to you. I have also coded a client side for managing the advisor configuration. The client side is based on my previous post Work Item cross-stream deliveries and promotion – a sample precondition. The result is something like this:

advisor

You can get all the code from here. It comes with usual lack of support or guarantee, but with the best of intentions.

Acknowledgedments: thanks to the SCM development team for providing useful hints in some areas of SCM code I wasn’t experienced with.

Advertisements

2 thoughts on “Unique Files Names SCM Advisor

  1. Nice post. I’m one of the RTC SCM developers, and I’d like to make a style suggestion. At the top of your advisor you do a few sanity checks:

    data instanceof IScmDeltaSource and deliverData.matchesOperationType(IScmProcessConfiguration.DELIVER_SERVER_OPERATION_ID). In each of those cases the advisor silently returns (which is equivalent to success). If either of those checks returns true, the operation has been misconfigured. I suggest failing the operation at that point. That will encourage your user to contact the administrator, so they can debug their process and see what’s wrong.

    • Good catch! Thanks Evan. Actually I was failing the operation for later checks but not for the initial ones you pointed out.
      I have corrected the downloadable code and the post snippets.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s