A Custom Condition to Make Attributes Required or Read-Only by Role Version 2


A new requirement came up. Prioritize mandatory over read only. Can that be done? It can, lets see how.

As described in A Custom Condition to Make Attributes Required or Read-Only by Role it is possible to create custom behavior for making attributes read-only and required. The solution above has a catch however. If you configure a user with roles that make an attribute mandatory and roles that make the same attribute read-only, the user can not modify the mandatory attribute and can not save the work item. This situation can also arise with the other built in advisors.

So the new requirement was: modify the condition in a way that it checks if an attribute is required for the user and not make it read only in that case, even if the user has roles that specify the attribute read only. This could also be done the other way around, really, or there could be other requirements.

Is this really doable? I had my doubts. It took me a while to find that it can actually be done. It can be done, but there are several tricks that have to be used to do so.

Solution Outline

The solution described here provides a refined version of the condition for role and state provider as outlined below. The code has been extended to be able to:

  1. Detect that a condition is for a read-only attribute
  2. Detect which attribute it is responsible for
  3. Recursively call the operational behavior that determines if an attribute is required and to determine based on the information retrieved if the attribute is required and the condition needs to return false without further checks to prevent to make a required attribute read-only

The solution will be explained in detail below, but first.

A warning that should be carefully considered

The code that comes with this solution is experimental. All content is provided “as is” and without warranties. I am not responsible for any harm or damage caused by this material to you, your computer, your software, or anything else. 8)

The code does not write to the data, so data corruption is not a concern.

However, the code uses a recursive approach to check if an attribute is required to decide if it can be made read only. This can have performance implications (as every extension can). Since the code has to go multiple times through all the information, the user experience could be impacted by slow loading of work item presentations.

The code has to be called every time the work item is

  • opened in an editor
  • saved anywhere

so this runs in the users UI context and could be noticed if its execution time is long. This obvioulsy depends on the number of attributes, work item types and roles. For A attributes and W work item types and R roles the maximum complexity is roughly 3*(A*W*R), for read only attributes because it has to go through two configurations for required attributes preconditions plus one run through the condition itself. So it is o(n) with n=A*W*R for read only attributes. It is A*W*R for required attributes also o(n) with n=A*W*R.

So complexity is linear, however, there is no free lunch and the code has to read potentially a lot of information and perform other operations, so there could be an impact on the user performance, especially if the server is under heavy load.

Since it is recursive, you also have to make sure the code terminates. In this case it is done by getting only the required attributes if the condition is for read only attributes. If you get this wrong in the configuration, the server will probably crash with an OOM error.

I did not see any performance problems when I tested it, but the test was only with a limited set of roles and configuration entries in a single user environment.
To be able to recursively call the operational behavior recursively, the code has to use internal API that can be changed without notice. My experience so far is that the API, also internal API is very stable, but this should be considered anyway.

The code has to make assumptions about the condition names for Read-Only conditions. This naming convention is necessary for the code to get the information it needs to work properly. If the naming convention is not followed carefully the code will not work as expected.

License and Download

The post contains published 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. I found a section relevant to source code at the and of the license. Please also remember, as stated in the disclaimer, that this code comes with the usual lack of promise or guarantee. Enjoy!

Download the Code

The code can be downloaded from DropBox here. Please note, there might be restrictions to access DropBox and the code in your company or download location.

Just Starting With Extending RTC?

If you just get started with extending Rational Team Concert, or create API based automation, start with the post Learning To Fly: Getting Started with the RTC Java API’s and follow the linked resources.

You should be able to use the code attached to this post in the development environment you set up in the Rational Team Concert Extensions Workshop and get your own extensions working there as well.

In this context, please also consider to at least read through the Process Enactment Workshop for the Rational solution for Collaborative Lifecycle Management lab 4 and lab 5 to understand how attribute customization works.

Solution Constraints

There are several constraints that will impact any possible solution. This is a list of the constraints I have experienced.

User UI Notification

The RTC UI uses a red asterisks to be able to provide the user a visible indication that a work item attribute is required.  The RTC UI also makes read-only attributes not changeable by removing the border or graying out the attribute value selection box. In this state the user can’t accidentally enter data. The image below shows the web UI with required and read only attributes. Severity is read only and can not be changed, a number of other attributes are required and show an asterisk.

UI required and read only attributes

RTC provides out of the box pre-conditions also called advisors for defining work item attributes read-only and required. It is important to know that advisors only run once the save button is pressed.

Running at save and showing information in the UI prior to the save is a contradiction. It should not be possible to show indications in the UI for a work item save advisor.

The way this is implemented in RTC is that the UI knows the built in advisors for Required Attributes for Type and State and for Condition and the Read-only Attributes for Type and State and for Condition. The UI calls these advisors to retrieve the information for the attributes before the advisors will actually run.

Note: This is hard coded into the UI and can not be extended. In fact it is not possible to show these UI indications for a custom work item save advisor.

We will use the same mechanism the UI uses in our solution below.

Conclusion on User UI Notification

It is possible to write a work item save advisor that prevents from saving a work item if attributes are not set or if they are changed, but these advisors would not be able to provide the user with any information in the UI up front. For this reason I believe the only feasible approach to implement behavior for read-only and mandatory attributes is to use custom conditions and using the advisors that make attributes required and read only based on condition values to provide visual feedback in the UI.

Otherwise the RTC developers would have to create an extension point, where custom advisors could provide the information which attributes are read only or required.

Conditions – Limitations

The condition is called with some data that allows it to run. This data contains

  1. The selected work flow action of the work item if available
  2. The label or name the condition has

There is no information available

  1. About the advisor in which context the condition is called: Required Attributes for Condition or Read-Only Requirement for Condition or a custom advisor, so the condition does not know if it is for read only or required.
  2. About the attribute or attributes the condition is responsible for in the context of the advisor

To summarize this: the condition can not decide with the given data

  1. If it is responsible for making an attribute read-only or mandatory
  2. Which work item attributes the condition acts upon

It is necessary to know in the condition, if it is for making a work item read only and it is necessary to know which attribute this condition is responsible for. This data is needed in the condition to perform the subsequent checks if the attribute is required. This means it is necessary to pass some information to the condition that allows it to get this data. The only possible way I am aware of at this time is the name/label of the condition.

This is the main reason for the design below, where the condition needs to be specified for one attribute only, has to have the mode of working and the attribute ID in the name and has to be configured individually with roles and workflows.

The initial attempt was to have only one condition with a central configuration. This approach was not feasible with the information available in the condition.

Solution Code

To achieve the requested functionality the condition code in the AbstractUserRoleTypeAndStateConditionProvider was enhanced in the following ways.

  1. A new method was added to test if the condition should exit with false based on the attribute being required
  2. The call is wrapped to enable and disable this recursive behavior using the constant ENABLE_RECURSIVE_REQUIRED_ATTRIBUTES . By default this is set to false. In this case the old behavior is maintained. To change this, the value must be set to true and the code must be rebuilt and deployed
  3. Additional supporting methods and data has been added to map attribute ID’s

It would be possible and simple to enhance the code of the condition to get the information if it should do the check from its configuration similar to how it gets the other configuration data.I decided to not spend the time for that. So the line below is responsible and needs to be changed to enable the new capability. Change the value to “true” to enable the recursive feature. Then built and deploy the extension.

Enable check for required

The check if a work item attribute is required and can therefor not be read only is the first check that the new behavior does. the rest of the code is absolutely identical to the old one. Only the methods necessary for this check has been added. The image below shows the code to call the new behavior.

Calling the Check

The new method looks like this:

/**
 * This method tries to extract the attribute name from a naming convention
 * 
 * The name of the condition has to match Readonly_the.id.of.the.attribute
 * 
 * If the naming convention holds, it converts the attribute ID for built-in
 * attributes into an internal form that is used by the UI and tries to
 * retrieve the required attributes for Type and State or by condition
 * 
 * It checks if the attribute it is responsible for is in the list If it is,
 * the attribute is considered to be required.
 * 
 * @param workItem
 * @param configuration
 * @param workItemCommon
 * @param monitor
 * @return returns true if the attribute could be identified as required
 *         false otherwise.
 * 
 * @throws TeamRepositoryException
 */
boolean isRequiredRecursive(IWorkItem workItem,
		IConfiguration configuration, IWorkItemCommon workItemCommon,
		IProgressMonitor monitor) throws TeamRepositoryException {
	/*
	 * Please note, there is no information available about the attribute ID
	 * this condition works for. It is necessary to provide it somehow.
	 * There is also no information available what mode this condition is
	 * used for i.e. is it for required attributes or for read-only
	 * attributes.
	 * 
	 * The code below assumes that the mode and the attribute ID is coded
	 * into the Label of the condition. The assumption is that the name of
	 * the condition follows this pattern:
	 * 
	 * Readonly_the.attribute.id
	 * 
	 * The label is split in the first part which is used to detect the
	 * condition is used for. Basically if the mode is for detecting
	 * read_only or required attributes The second part is used to identify
	 * the attribute ID the condition is used for.
	 * 
	 * If the condition is used to make an attribute read only, it tests if
	 * the attribute is really required due to any other rule.
	 * 
	 * If it is required it can't be set to read only and the code returns
	 * true.
	 */
	String label = configuration.getLabel();
	String[] attrib_Info = label.split("_");
	String operation = null;
	String attributeID = null;
	if (attrib_Info.length > 1) {
		operation = attrib_Info[0].trim().toLowerCase();
		attributeID = attrib_Info[1].trim();
		// get the internal representation of the attribute, if there is one
		attributeID = getInternalID(attributeID);
	}

	// Can we check?
	if (!(operation != null && attributeID != null && operation
			.equals("readonly"))) {
		return false;
	}
	/*
	 * There is enough data to check if there is a required attribute. In
	 * order to do this, call the existing configured operational behaviour.
	 */
	Set requiredAttributes = new HashSet();
	IAuditableCommon auditableCommon = workItemCommon.getAuditableCommon();
	IProcessAreaHandle processArea = workItemCommon.findProcessArea(
			workItem, monitor);
	IAuditableCommonProcess process = auditableCommon.getProcess(
			processArea, monitor);

	// Get the workflow action
	String actionId = configuration.getProviderContext()
			.getWorkflowAction();

	/*
	 * Call the RequiredAttributesByConditionAdvisor advisors recursively to
	 * get the list of attributes that are required for this context by
	 * condition
	 * 
	 * @see com.ibm.team.workitem.common.internal.
	 * RequiredAttributesByTypeAndStateAdvisor
	 * 
	 * Note, there seems to be an issue when running this on the Eclipse
	 * client in debug mode.
	 */
	List advisorDeclarations1 = process
			.findAdvisorDeclarations(processArea,
					WorkItemCommon.WORK_ITEM_SAVE_OPERATION_ID,
					RequiredAttributesByConditionAdvisor.ADVISOR_ID,
					monitor);
	for (IAdvisorDeclaration advisorDeclaration1 : advisorDeclarations1) {
		requiredAttributes.addAll(RequiredAttributesByConditionAdvisor
				.getRequiredAttributes(workItem, workItemCommon, actionId,
						advisorDeclaration1.getConfigurationElement(),
						false, monitor));
	}
	/*
	 * Call the RequiredAttributesByConditionAdvisor advisors recursively to
	 * get the list of attributes that are required for this context by type
	 * and state.
	 * 
	 * @see com.ibm.team.workitem.common.internal.
	 * RequiredAttributesByTypeAndStateAdvisor
	 * 
	 * Note, there seems to be an issue when running this on the Eclipse
	 * client in debug mode.
	 */
	List advisorDeclarations2 = process
			.findAdvisorDeclarations(processArea,
					WorkItemCommon.WORK_ITEM_SAVE_OPERATION_ID,
					RequiredAttributesByTypeAndStateAdvisor.ADVISOR_ID,
					monitor);
	for (IAdvisorDeclaration advisorDeclaration2 : advisorDeclarations2) {
		requiredAttributes.addAll(AbstractAttributesByTypeAndStateAdvisor
				.getConfiguredAttributes(workItemCommon, workItem,
						actionId,
						advisorDeclaration2.getConfigurationElement(),
						monitor));
	}
	// Is the attribute in the list of required attributes?
	if (!requiredAttributes.isEmpty()) {
		if (requiredAttributes.contains(attributeID)) {
			// If the attribute is in the list, don't make it read only.
			return true;
		}
	}
	return false;
}

What it does is essentially as follows.

The first section is used to decode information from the label or name of the condition. The subsequent code needs to know if this condition is to make an attribute read only. If this is the case it needs to know the name of the attribute it is responsible for.

The first block checks if the label of the condition follows the naming conventions. If it does, the label looks like i.e. Readonly_com.ibm.team.workitem.attribute.description. The condition uses this information to determine if the condition is for making an attribute read only and what the id of the attribute is. To do that, it splits the label of the condition using the underscore character ‘_’. If the split has at least two parts, it uses the first part as the indicator for what the condition is used for. The second part is expected to contain the attribute ID for the attribute the condition is responsible for.

The attribute ID, if one was found, is passed through the method getInternalID(). This method converts the ID’s for built in attributes into the internal representation that is available later in the process. If there is no internal ID found for an attribute, the original ID is returned. This mapping is necessary, because many of the built in attributes have an ID that is shown externally in the process configuration editor and an internal id that is used in a lot of the internal processing.

As an example, in the process configuration the attribute Description shows up as com.ibm.team.workitem.attribute.description but the internal representation is description. See the image below.

Internal and external ID for built in attributes

The method and the mapping is explained later in the document.

The next part of the code checks if there is enough data to decide if the current condition is a read-only condition and there is an attribute that can be checked. If the condition label is not matching the conventions there is not enough data available to decide and the method returns false.

In the next steps a HashSet for attribute ID’s is created and the code gets the required API classes that are required for the final steps.

The code then gets the process configurations for the RequiredAttributesByConditionAdvisor advisor and retrieves the list of required attributes from them. This is basically the code that the RTC UI uses itself to determine the read only and the required attributes from the built in advisors.

The code does the same for the RequiredAttributesByTypeAndStateAdvisor configurations.

Once all required attributes are available in the HashMap, the code tests if the attribute that this condition is supposed to check for being read only is in the list. If that is the case the code returns true, false otherwise.

If this method returns false, the normal behavior described in A Custom Condition to Make Attributes Required or Read-Only by Role is executed to determine if the condition should return true (to make the attribute read only or required). If the method returns true, the condition detected it was responsible to make the attribute read-only and the attribute is required and therefore must not be set to read only. In this case the whole condition can return false and does not have to check anything else.

The method getInternalID is very simple and looks like this.

/**
 * Try to get the internal representation for an attribute ID If there is an
 * internal representation, return it, otherwise return the original
 * 
 * @param attributeID
 * @return
 */
private String getInternalID(String attributeID) {
	String newVal = iDMap.get(attributeID);
	if (null != newVal)
		return newVal;
	return attributeID;
}

The method createMap to create the map to look up the internal Id looks like below.

/**
 * Create the built-in attribute mapping of external ID's to the internal
 * representation that can safely be used to compare the required
 * attributes.
 * 
 * @return the map
 */
private static HashMap<String, String> createMap() {
	HashMap<String, String> map = new HashMap<String, String>(40);
	map.put("com.ibm.team.workitem.attribute.severity",
			IWorkItem.SEVERITY_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.priority",
			IWorkItem.PRIORITY_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.version",
			IWorkItem.FOUND_IN_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.id", IWorkItem.ID_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.workitemtype",
			IWorkItem.TYPE_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.projectarea",
			IWorkItem.PROJECT_AREA_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.summary",
			IWorkItem.SUMMARY_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.state",
			IWorkItem.STATE_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.creator",
			IWorkItem.CREATOR_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.owner",
			IWorkItem.OWNER_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.description",
			IWorkItem.DESCRIPTION_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.creationdate",
			IWorkItem.CREATION_DATE_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.resolutiondate",
			IWorkItem.RESOLUTION_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.duedate",
			IWorkItem.DUE_DATE_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.duration",
			IWorkItem.DURATION_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.correctedestimate",
			"correctedEstimate");
	map.put("com.ibm.team.workitem.attribute.timespent", "timeSpent");
	map.put("com.ibm.team.workitem.attribute.category",
			IWorkItem.CATEGORY_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.target",
			IWorkItem.TARGET_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.resolver",
			IWorkItem.RESOLVER_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.resolutiondate",
			IWorkItem.RESOLUTION_DATE_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.tags", IWorkItem.TAGS_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.modified",
			IWorkItem.MODIFIED_PROPERTY);
	map.put("com.ibm.team.workitem.attribute.modifiedby",
			IWorkItem.MODIFIED_BY_PROPERTY);
	return map;
}

It is called when the class is instantiated.

/**
 * A map that is used to translate the external representation of a built-in
 * attribute into the one that is used internally
 */
HashMap<String, String> iDMap = createMap();

It is possible to add custom mappings here.

The following mapping table shows the values of the attribute Name, the internal ID of the attribute and the external ID used in the process configuration.

Attribute ID mapping built in attributes 1

The following attributes are also available but should be considered carefully. Most of these attributes are pseudo attributes like “Subscribed By” or automatically set by the process. Carefully consider if these attributes should be made required or read-only.

Attribute ID mapping built in attributes 2

Do not use this condition to make the Status attribute internalState read only. Only use the built in permissions to define which role can perform which workflow action.

Please note that the Status of the work item is especially critical. The condition evaluates the status of the work item based on the current state and the action selected. If there is an action selected, the state of the work item is calculated as the target state of that action. In case the attribute is now read only and not required, it is basically impossible to save the work item – as designed. However, there is a built in permission for work flow ations per role that should be used.

Configuring the Conditions
The configuration of the conditions works as described in A Custom Condition to Make Attributes Required or Read-Only by Role, except for some details for conditions responsible for read-only attributes.

If you create the condition to make an attribute read-only

  1. The label has to follow the naming condition Readonly_ where is the internal or external attribute ID
  2. The attribute ID has to be correct and is case sensitive; any typo renders the condition not functional
  3. Each condition can only be configured for one attribute and not for a group of attributes

In the example below, the label for required attributes can be chosen arbitrarily I.e. Required_Filed_Against. The Conditions for read only attributes have to follow the naming condition using an external ID i.e.

  1. Readonly_com.ibm.team.workitem.attribute.description
  2. Readonly_com.ibm.team.workitem.attribute.category
  3. Readonly_internalTags.

The 3rd example uses an internal ID instead of the external ID.

Create conditions for attributes

The names of the conditions are reflected in the configuration of the operational behavior.

Configure the Read Only For condition advisor

The result should look like below.

Configure the Read Only For condition advisor - result

The following table shows an example configuration for the specific user roles of a user in the new state for the work item type defect. Several attributes are configured as read-only and required in the available advisors using a condition or using the built in advisors.

Example configuration

The following screen shot shows the resulting display in the web UI if the new condition is used.

Result of the configuration in the UI

Please note that only the severity is read only, because all other attributes that happen to be configured as read only using this condition are changeable, because they are required.

Please note, the built in advisors don’t follow this pattern. If you configure an attribute required and read only for a user, the behavior is like using the condition version with the recursive behavior switched off.

Configuring and Deploying the Condition

Please follow the post A Custom Condition to Make Attributes Required or Read-Only by Role for more details. This version is only a small extension to the original condition and the description there applies to this condition as well.

Summary

Using a custom condition and the out of the box operational behavior for Required Attributes For Condition and Read-Only Attributes For Condition, allows to achieve the required behavior.

Keep in mind this is by no means production code. You might want to do more testing.

As always I hope this helps someone out there to get their job done more efficient.

Advertisements

About rsjazz

Hi, my name is Ralph. I work for IBM and help colleagues and customers with adopting the Jazz technologies.
This entry was posted in Jazz, RTC Automation, RTC Extensibility and tagged , , , . Bookmark the permalink.

13 Responses to A Custom Condition to Make Attributes Required or Read-Only by Role Version 2

  1. Pingback: A Custom Condition to Make Attributes Required or Read-Only by Role | rsjazz

  2. Drake says:

    How to test this plugin, because in article they didn’t share much more information about testing.
    Anybody have knowledge please share info.

    • rsjazz says:

      Cite:

      Configuring and Deploying the Condition

      Please follow the post A Custom Condition to Make Attributes Required or Read-Only by Role for more details. This version is only a small extension to the original condition and the description there applies to this condition as well.

  3. Drake says:

    Thanks Ralph, this is server API or Plug-In. Both have the same process for deployment and testing?

  4. Gautham says:

    I have requirement to get the User Roles, WorkItem state & roles from the current workitem. I am very new to this, please share some information about the implementation to get user, user roles, workitem state, current state roles.

  5. Shiva says:

    Hi Ralph, I am not clear in that and i searched lot of articles shared by you also.
    matches() is the entry point of this plugin?
    My requirement is “need to restrict the modification in attributes depends up on the role & current user.” , want to perform this action when click the SAVE button. So I am implements IOperationAdvisor and override the run() method, from run() method am calling the matches() method for to get the contributor roles and state roles.
    But I can’t able to set the value and pass the Iconfiguration as parameter to matches().
    Please clarify me, my working path is correct or not.
    Please help me in this point, no body is here to solve this issue.

    • rsjazz says:

      Again, grab the code you need to get the role of the user and put it into your plugin. You can’t just call the matches method, because that assumes to be called in a condition and you basically don’t have any of the needed data available in your advisor.

  6. Irfhan says:

    Is it possible to make the ‘save’ editor button as read only based upon the state or roles

  7. Irfhan says:

    Hi Ralph, I try to make the status attribute as read only, when i am in an unauthorized state.
    So I added the status attribute in precondition.
    For example:- I don’t have access in State2. So I can’t able to modify the attributes in state2.
    1. In preconditions, I added the status attributes and priority attribute.
    2. I got Attribute ‘Status’ is not modifiable, when I tried to change the State1 to State2 on save editor button action.
    3. So I can’t able to move from state1 to state2.Then I removed the status attributes from precondition and saved the workitem from state1 to state2.
    4. In state2, I can able to make read only all the attributes except “Status Attribute” and “Save Editor button”. So I can able to change the state from state2 to any state, as per logic its not correct.Because I don’t have access in State2.

    And I tried to make the save button as disable upon the roles. To make Save editor button disable is not possible.

    Please share your suggestion in this point.

    • Irfhan says:

      Ralph. Sorry to post my questions here. When i tried to post my questions in jazz forums its shows certification error.

      • rsjazz says:

        Last answer:

        The condition calculates the state of the work item from the current state and the selected workflow action.
        If you select a workflow action the target state will be different from the current one and the required/read only attributes will be different one.

        If you design your process to make the state read only and it does not work for you, your process is broken.

        I can’t provide any more insight.

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