Constraining Models

The purpose of constraining models is to ensure that they never end up in an invalid state. There are a number of options available for constraining models in the UML modeler.

Registering a Live Validation Constraint

The following live validation constraint will prevent a user from changing the name of any UML named element into a name that isn't a valid Java identifier. Any attempt to use an invalid name will be undone.


   <extension
         point="org.eclipse.emf.validation.constraintProviders">
      <category
            id="com.ibm.constraintCategory"
            name="My Category"/>
      <constraintProvider cache="true">
         <package namespaceUri="http://www.eclipse.org/uml2/3.0.0/UML"/>
         <constraints categories="com.ibm.constraintCategory">
            <constraint
                  class="someconstraints.JavaNameConstraint"
                  id="javaNameConstraint"
                  lang="Java"
                  mode="Live"
                  name="Enforce Valid Java Names"
                  severity="ERROR"
                  statusCode="1001">
               <target class="NamedElement">
                  <event name="Set">
                     <feature name="name"/>
                  </event>
                  <event name="Unset">
                     <feature name="name"/>
                  </event>
               </target>
               <message>
                  Element with name {0} does not have a valid Java name.
               </message>
               <description>
                  This constraint ensures that all named elements in a model have valid Java identifiers for a name.
               </description>
            </constraint>
         </constraints>
      </constraintProvider>
   </extension>

The first part of the above extension creates a new constraint category. These categories are used to organize constraints into a tree for the model validation preference page. A constraint provider will be providing constraints organized into this category for the UML2 metamodel. The language of the constraint is Java, which is written into the "com.ibm.javaNameConstraint" class. The mode of the constraint is "Live" and its severity is "ERROR", which means that the constraint will directly undo changes that violate the rule. A severity of "WARNING" simply warns users if their action violates the rule. This constraint targets any set or unset events of the name of any UML2 NamedElement (and subtypes). Finally, the "{0}" is a substitution variable that the Java code is used to give the name of the element that has been given the invalid name.


public class JavaNameConstraint extends AbstractModelConstraint {
	public IStatus validate(IValidationContext ctx) {
		NamedElement element = (NamedElement)ctx.getTarget();
		
		if (!isJavaIdentifier(element.getName())) {
			return ctx.createFailureStatus(new Object[] {element.getName()});
		}
		
		return ctx.createSuccessStatus();
	}
	
	private boolean isJavaIdentifier(String name) {
		final int length = name.length();

		if (length > 0 && !Character.isJavaIdentifierStart(name.charAt(0))) {
			return false;
		}

		for (int i = 1; i < length; i++) {
			if (!Character.isJavaIdentifierPart(name.charAt(i))) {
				return false;
			}
		}
		
		return true;
	}
}

The validate method returns one of two results, a success status or a failure status. In the failure case, it provides the element's name to replace the "{0}" in the message declared above. Note that this constraint, as written, will be executed in batch mode as well as live mode. If the user gestures to validate an existing model that they have opened it will find elements that already have an invalid name.

Registering a Batch Validation Constraint

A batch validation constraint is written in a similar way to a live constraint. The main differences are in the way that it is registered. The following extension shows how the above Java code could be registered for only batch mode.


   <extension
         point="org.eclipse.emf.validation.constraintProviders">
      <category
            id="com.ibm.constraintCategory"
            name="My Category"/>
      <constraintProvider cache="true">
         <package namespaceUri="http://www.eclipse.org/uml2/3.0.0/UML"/>
         <constraints categories="com.ibm.constraintCategory">
            <constraint
                  class="someconstraints.JavaNameConstraint"
                  id="javaNameConstraint"
                  lang="Java"
                  mode="Batch"
                  name="Enforce Valid Java Names"
                  severity="ERROR"
                  statusCode="1001">
               <target class="NamedElement"/>
               <message>
                  Element with name {0} does not have a valid Java name.
               </message>
               <description>
                  This constraint ensures that all named elements in a model have valid Java identifiers for a name.
               </description>
            </constraint>
         </constraints>
      </constraintProvider>
   </extension>

The mode of this registration is "Batch". Also, there is no need to declare any events against the NamedElement target because batch constraints are not triggered by specific changes to the target object but rather the type of the target object.

Registering an OCL Validation Constraint

Constraints can also be written in the OCL language. The following extension is the equivalent to the above batch constraint except written in OCL.


   <extension
         point="org.eclipse.emf.validation.constraintProviders">
      <category
            id="com.ibm.constraintCategory"
            name="My Category"/>
      <constraintProvider cache="true">
         <package namespaceUri="http://www.eclipse.org/uml2/3.0.0/UML"/>
         <constraints categories="com.ibm.constraintCategory">
            <constraint
                  id="javaNameConstraint"
                  lang="OCL"
                  mode="Batch"
                  name="Enforce Valid Java Names"
                  severity="ERROR"
                  statusCode="1001">
<![CDATA[       
               let validChars : Set(String) = Set{'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','_','0','1','2','3','4','5','6','7','8','9'} in
Sequence{1..self.name.size()}->forAll(i : Integer  | validChars->includes(self.name.toLower().substring(i,i)))
]]>
              <target class="NamedElement">
               </target>
               <message>
                  Element does not have a valid Java name.
               </message>
               <description>
                  This constraint ensures that all named elements in a model have valid Java identifiers for a name.
               </description>
            </constraint>
         </constraints>
      </constraintProvider>
   </extension>

Programmatically Validating an EObject

Sometimes, it is useful to invoke batch validation on a single EObject programmatically. This would allow one to verify, for example, that a particular constraint is not being violated by a UML modeling object.


IBatchValidator validator = 
	(IBatchValidator)ModelValidationService.getInstance()
		.newValidator(EvaluationMode.BATCH);

validator.setInclueLiveConstraints(false);
validator.setReportSuccesses(true);
validator.setTraversalStrategy(new ITraversalStrategy.Flat());

IStatus status = validator.validate(selection, new NullProgressMonitor());

if (status.isMultiStatus()) {
	IStatus[] results = status.getChildren();
	
	for (int i=0; i<results.length; i++) {
		if (results[i] instanceof IConstraintStatus) {
			IConstraintStatus constraintResult = (IConstraintStatus)results[i];
			
			if (constraintResult.getConstraint().getDescriptor().getId()
					.equals("com.ibm.foo.javaNameConstraint")) {
				
				if (!constraintResult.isOK()) {
					// The object violated the constraint
				} else {
					// The object passed
				}
			}
		}
	}
}

The above code will invoke batch and live constraints in the UML modeler context in batch mode. If live constraints should not be invoked then the validator org.eclipse.emf.validation.service.IBatchValidator.setIncludeLiveConstraints() method should be set to exclude the live constraints. The org.eclipse.emf.validation.service.IValidator.setReportSuccesses() is set to true so that it can be verified that the particular constraint was actually called. Otherwise, it is not possible to differentiate between a successful validation of the object or that the constraint was disabled through the preference page by the user. To prevent the batch validator from validating the children of the EObject the org.eclipse.emf.validation.service.IBatchValidator.setTraversalStrategy() method is called with a "FLAT" traversal strategy.

The validator's status will usually be a multi status because the validator must report successes as well as as failures and there is usually more than one constraint applied to a particular EObject. Also, the validator will always use IConstraintStatus objects for each of its results so that additional information can be gathered (e.g. constraint ID and result locus). The code effectively verifies that the status for the particular constraint has an "OK" status to determine that it has passed.

Using a Trigger Listener to Correct Problems Automatically

Validation constraints are useful to prevent problems before they occur. In some cases, code can be written to take into account the change that has been made to a model and correct the problem. In the following example, a trigger listener is registered that will automatically replace all of the invalid characters in the element's name with "_".


   <extension
         point="org.eclipse.emf.transaction.listeners">
      <listener class="com.ibm.foo.NamedElementListener">
         <editingDomain id="org.eclipse.gmf.runtime.emf.core.compatibility.MSLEditingDomain"/>
      </listener>
   </extension>

The listener is registered against the editing domain used by the UML Modeler, which can be found here. It will be notified of all changes made in that domain.


public class NamedElementListener extends TriggerListener {
	protected Command trigger(TransactionalEditingDomain domain,
			Notification notification) {
		
		if (notification.getNotifier() instanceof NamedElement) {
			if (notification.getFeature() == UMLPackage.eINSTANCE.getNamedElement_Name()) {
				final NamedElement element = (NamedElement)notification.getNotifier();
				
				final StringBuffer fixedName = new StringBuffer();
				for (int i=0; i < element.getName().length(); i++) {
					if (!Character.isJavaIdentifierPart(element.getName().charAt(i))) {
						fixedName.append('_');
					} else {
						fixedName.append(element.getName().charAt(i));
					}
				}
				
				if (!fixedName.toString().equals(element.getName())) {
					return new EMFOperationCommand(domain, new AbstractTransactionalCommand(domain, "Clean name", null) {

						protected CommandResult doExecuteWithResult(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
							element.setName(fixedName.toString());
							
							return CommandResult.newOKCommandResult();
						}
					});
				}
			}
		}
		
		return null;
	}
}

Trigger listeners do not make their changes to the model directly. Instead, they must return the changes in the form of a command. For performance reasons, if no change needs to be made, the listener should return null. The above code returns an org.eclipse.emf.workspace.EMFOperationCommand when it could just as easily returned back an EMF SetCommand, for example. The use of this operation command was to illustrate how to wrap a GMF ICommand as a valid result including all of its facilities (e.g. affected files).

Preventing the Deletion of Modeling Elements

The prevention of the deletion of modeling elements poses some problems for trigger listeners and live validation constraints. By the time that these entities are called, the element is already deleted and most of its characteristic attributes have been removed. For example, a UML2 class with a specific name will not have any name by the time that a validation constraint is notified of the deletion of the class. Also, that class will no longer be located in its parent package by the time that a trigger listener is called, making it difficult to determine where it was previously located and what its previous values were in order to undo the changes made to it.

Edit helper advice allows one to identify particular types of elements (element types) and provide advice on how they should be created, modified and deleted. One form of advice is the prevention of some of these modifications. In order to prevent the deletion of a particular type of modeling element, an advice can be registered that will veto the deletion. The example below will prevent the deletion of any UML2 named element in the UML Modeler that has a name that complies with standard Java naming convention.


   <extension
         point="org.eclipse.gmf.runtime.emf.type.core.elementTypes">
      <metamodel nsURI="http://www.eclipse.org/uml2/3.0.0/UML">
         <adviceBinding
               class="com.ibm.foo.NamedElementAdvice"
               id="com.ibm.foo"
               inheritance="all"
               typeId="com.ibm.xtools.uml.namedElement"/>
      </metamodel>
   </extension>
   
   <extension
         point="org.eclipse.gmf.runtime.emf.type.core.elementTypeBindings">
      <binding context="com.ibm.xtools.uml.type.context">
         <advice ref="com.ibm.foo"/>
      </binding>
   </extension>

The registration above must be constructed carefully in order for the advice to be invoked. In this case, the modeling elements that are being advised on are UML2 elements, so the UML2 namespace URI is declared in the "metamodel" element. An advice binding is provided in order to bind a particular type of advice with a particular type of element.

The ID of this advice binding is "com.ibm.foo" and it is not included in the modeler's client context, which means that it will not be included in the UML modeler editing operations. This binding must be included in the modeler's client context. The second extension is responsible for linking the advice to this client context ("com.ibm.xtools.uml.type.context").

In order to ensure that the advice is called on only elements with a name it is registered against the "com.ibm.xtools.uml.namedElement" element type. It should never be called on any UML2 elements that are not named elements.


NamedElementAdvice.java:
public class NamedElementAdvice implements AbstractEditHelperAdvice {
	public boolean approveRequest(IEditCommandRequest request) {
		if (request instanceof DestroyElementRequest) {
			DestroyElementRequest destroyRequest = (DestroyElementRequest)request;
			
			NamedElement element = (NamedElement)destroyRequest.getElementToDestroy();
			
			String name = element.getName();
			
			if (name != null) {
				for (int i=0; i<name.length(); i++) {
					if (!Character.isJavaIdentifierPart(name.charAt(i))) {
						return true;
					}
				}
				return false;
			}
		}
		return true;
	}
}

The advice itself only serves to veto certain types of deletes that are attempted by the user. It does not introduce new edit semantics. As a result, it must only override the approveRequest method and provide false in cases where it must veto the operation. The advice first checks that the request is in fact a destroy (delete) element request. Then it proceeds to analyze the name of the named elements slated for destruction to determine if it had followed the java naming requirements. If so, it vetos the operation.


Legal notices