/******************************************************************************* * Copyright (c) 2006 Oracle Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Oracle Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.bpel.validator.model; /** * Java JDK dependency */ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.namespace.QName; /** * BPEL Validation model dependency */ import org.eclipse.bpel.validator.model.Rules.Rule; /** * The base Validator class. * * A validator basically encapsulates an INode and runs some special methods (called rules) * to check the INode element. The output is a set of IProblem (s). * * <p> * * Rules are special methods that are discovered by reflection in two ways: * <ol> * <li> They either have form "rule_<name>_<index>", or * <li> The have the ARule annotation on them. * </ol> * * <p> * * Order of execution of the rules on a given validator is user defined - * that's what the "index" means above. A rule may also be turned off by * the validator code during execution, so that logically exclusive conditions * can be simply "turned" off by rules. * <p> * If the ARule annotation is used then the order() method returns the order of execution. * Rules are simply discovered for every validator (only once), then sorted, and then run * during invocations. * * <p> * * Beyond that, there are just 2 other items that govern how rules are executed. * <ol> * <li> The rule tag (simple string), and * <li> the arguments (if any) to the rule method. * </ol> * <p> * Rule tags are just strings which help organize the rules in some way and force their execution * at specific times. There are two tags reserved for the system, one is "pass1", the other is "pass2". * User that write validators can invoke other rule sets by calling {@see runRules() } * and passing the appropriate tag and arguments. Return values from rule methods are never used. * * <p> * Validators can be chained, so that you have the following scenario: * <pre> * 1 <-> 2 <-> 3 <-> 4 ... N * </pre> * For every INode there is the first validator that is always created. Other validators for the same * INode can be created by simply calling the factory and then attaching the returned validator to the chain. * This allows for separation of concerns. For example, "query" nodes may validate in the BPEL and WSDL contexts, and * also validate in the Query language context (the same physical node). * * <p> * * Validators can keep certain state between passes, in the data hash map which is available to * the super-classes. This data is erased every time a "pass1" tag is triggered on the validator but * remains on for the duration of the object's lifetime. * * <p> * This simple hash map mechanism is the way that various validators pass data to each other. This is primitive * but allows for very loose coupling between the code. When a validator asks for getData() the query goes * to its state data and if not found travels in the validator chain in the opposite direction to * execution (always to previous). * * <p> * And finally a note about INode. INode represents the generic tree node that some source material * is sitting behind. This could be BPEL of course, but it could be any other thing as well. There are * adapters which adapt DOM nodes to INodes and EMF nodes to INodes (though there is fewer of those). * * @author Michal Chmielewski (michal.chmielewski@oracle.com) * @date Sep 14, 2006 * */ @SuppressWarnings({"unchecked","nls","boxing"}) public class Validator implements IConstants { Logger mLogger = Logger.getLogger( getClass().getName() ); /** The runner which will run our rules */ RuleRunner fRuleRunner; /** Problems produced by these rules */ private List<IProblem> fProblems = new ArrayList<IProblem>(4); /** An empty problem array */ final static IProblem EMPTY_ARRAY[] = {}; /** The node that we are validating */ protected INode mNode ; /** Answer interesting things about the model */ protected IModelQuery mModelQuery; /** a list of state information that any validator can keep */ private Map<Object,Object> mData = new HashMap<Object,Object>(5); /** The selector that can be used to query the INode facade */ static protected Selector mSelector = new Selector(); /** Pass 1 tag for rules */ static public final String PASS1 = "pass1"; /** PAss 2 tag for rules */ static public final String PASS2 = "pass2"; /** Support chains of validators for the same element */ private Validator fNext = null; private Validator fPrev = null; /** A set denoting the list of Static Analysis checks actually done */ private Set<ARule> mSAChecks = null; /** * Create an instance of the validator. * Discover and setup the rules that we will be running. Primarily this * includes roaming through the methods and ordering them in the correct * order. */ protected Validator () { fRuleRunner = new RuleRunner ( this ); } /** * @param node */ public void setNode (INode node) { mNode = node; } /** * Set the model query. * @param query */ public void setModelQuery (IModelQuery query) { mModelQuery = query; } /** * Use this set to determine coverage of the validators. * * @param ruleSet */ public void setSAChecks ( Set<ARule> ruleSet ) { mSAChecks = ruleSet; } /** * Add a validator to the chain. It is always added to the end of the validator chain. * The validators form a chain starting at the very first one like so * <pre> * 1 <-> 2 <-> 3 <-> 4 <-> 5 ... N * </pre> * * When the main dispatcher code runs the validator for the given node, it starts running it at 1 * and continues to N. * <p> * A validator can be attached in 2 ways * <ul> * <li>During the initial factory create calls where all validators for the node * are created and the initial chain is built, and * <li>During execution of a rule. * </ul> * For that reason it is important to realize that the validator mNode, mModelQuery * references may not be yet set. * * @param next */ protected void attach ( Validator next ) { // no duplicates in chain if (this == next) { return ; } if (fNext == null) { fNext = next; next.fNext = null; next.fPrev = this; } else { fNext.attach ( next ); } } /** * Validate the node using the rules provided in this validator. * * @param tag the rules marked with the tag will be run * model */ final public void validate ( String tag ) { try { if (tag.equals(PASS1)) { start(); } fRuleRunner.runRules ( tag ); if (tag.equals(PASS2)) { end(); } } catch (Throwable t) { mLogger.logp(Level.SEVERE, getClass().getName(), "validate", "Problem executing this validator.", t); } if (fNext != null) { fNext.validate(tag); } } /** * Return the problems found as a result of validation * in the last validation pass. * * @return the list of problems found */ @SuppressWarnings("unchecked") final public IProblem[] getProblems () { // Next is empty, just return what we have. if (fProblems.size() == 0) { return EMPTY_ARRAY; } return fProblems.toArray( EMPTY_ARRAY ); } /** * * Return true if this node validator has captured any problems regarding * the node in question. * * Chained validators are also consulted. * * @return true if there are problems, false if there are no problems reported. * */ public boolean hasProblems () { return fProblems.isEmpty() == false; } /** * * @param node * @return true if there are problems, false otherwise. */ public boolean hasProblems ( INode node ) { if (isDefined(node)) { Validator validator = node.nodeValidator(); if (validator != null) { return validator.hasProblems(); } return false; } return true; } /** * Disable all rules. */ protected void disableRules ( ) { disableRules(0,65536); } /** * @param startIdx * @param endIdx */ protected void disableRules (int startIdx, int endIdx ) { fRuleRunner.addFilter ( new Rules.IndexFilter ( startIdx,endIdx) ); } /** * Start the validation pass. This is run before all the rules are run. * */ protected void start () { /** reset any disabled rules */ fRuleRunner.start(); /** * If we are the first validator in the chain * Then we reset any problems list and clear any data that we * have. */ if (fPrev == null) { fProblems.clear(); mData.clear(); } else { /** * We point ourselves at the first validator in the chain. * */ Validator first = getFirst(); /** * These are in fact shared between the validators * in the chain. */ fProblems = first.fProblems; mData = first.mData; mNode = first.mNode; mModelQuery = first.mModelQuery; mSAChecks = first.mSAChecks; } } /** * The validation pass has ended for this object */ protected void end ( ) { } /** * Runs the rules matching the tag and and the arguments given. * * @param tag * @param args */ protected void runRules ( String tag, Object ... args ) { fRuleRunner.runRules (tag,args); } /** * Add problems derived from contained validators into the * problems we are reporting. * * @param problems */ protected void addProblems ( IProblem[] problems ) { for(IProblem p : problems) { fProblems.add(p); } } @ARule( author = "michal.chmielewski@oracle.com", desc = "Internal error: no validator.", date = "10/2/2006", sa = -1, warnings="BPELC__INTERNAL" ) protected void internalProblem ( Rule rule, Throwable t ) { IProblem problem = createWarning(); while ( t.getCause() != null ) { t = t.getCause(); } problem.fill("BPELC__INTERNAL", toString(mNode.nodeName()), rule.getFullName(), rule.getIndex(), rule.getTag(), t); problem.setAttribute(IProblem.EXCEPTION, t); } /** * Mark that an SA check has been performed. * This is done automatically when create*() methods are called to * create an error/warning/info but you can call this method directly * too. * * The is purely for testing reasons to make sure that cases * are correctly run. This method may be a noop if the validator does not * have the property "internal.sa.checks" set to the right value. * * @param arule the annotation to record as having been executed */ protected void markSAExecution (ARule arule) { if (mSAChecks == null) { return ; } if (arule == null) { arule = fRuleRunner.getExecutingRule().method.getAnnotation( ARule.class ); } if (arule != null) { mSAChecks.add(arule); } } /** * Adopt this problem as one of ours. In this case, we simply fill in the location information * in the problem and add it to a list of problems that we keep. * * @param problem * @param node */ public void adopt ( IProblem problem , INode node ) { // remember it fProblems.add (problem); problem.setAttribute(IProblem.NODE, node ); problem.setAttribute(IProblem.LINE_NUMBER, mModelQuery.lookup(node, IModelQueryLookups.LOOKUP_NUMBER_LINE_NO,-1)); problem.setAttribute(IProblem.COLUMN_NUMBER, mModelQuery.lookup(node, IModelQueryLookups.LOOKUP_NUMBER_COLUMN_NO,-1)); problem.setAttribute(IProblem.CHAR_END, mModelQuery.lookup(node, IModelQueryLookups.LOOKUP_NUMBER_CHAR_END,-1)); problem.setAttribute(IProblem.CHAR_START, mModelQuery.lookup(node, IModelQueryLookups.LOOKUP_NUMBER_CHAR_START,-1)); problem.setAttribute(IProblem.LOCATION, mModelQuery.lookup(node, IModelQueryLookups.LOOKUP_TEXT_LOCATION,null,null)); problem.setAttribute(IProblem.ADDRESS_MODEL, mModelQuery.lookup(node, IModelQueryLookups.LOOKUP_TEXT_HREF,null,null)); problem.setAttribute(IProblem.ADDRESS_XPATH, mModelQuery.lookup(node, IModelQueryLookups.LOOKUP_TEXT_HREF_XPATH,null,null)); } /** * Create a problem based on the context node passed. * * @param node the context node from which the problem will be * created. * @return the IProblem marker for the node indicated. */ protected IProblem createProblem ( INode node ) { IProblem problem = new Problem ( this ); adopt (problem, node); Rule r = fRuleRunner.getExecutingRule(); if (r != null) { problem.setAttribute(IProblem.RULE, r.getFullName() ); ARule a = r.method.getAnnotation( ARule.class ); if (a != null) { markSAExecution(a); problem.setAttribute( IProblem.SA_CODE, a.sa() ); problem.setAttribute( IProblem.RULE_DESC, a.desc() ); } } return problem; } /** * Create an error problem on the current node * * @return the problem created */ protected IProblem createError ( ) { return createError ( mNode ); } /** * Create an error problem. This does the same as this as * createProblem plus it sets the problem object to severity error. * * @param node * @return the problem to be recorded. */ protected IProblem createError ( INode node ) { IProblem problem = createProblem (node); problem.setAttribute(IProblem.SEVERITY, IProblem.SEVERITY_ERROR); return problem; } /** * Create a warning problem on the current node * * @return the problem created */ protected IProblem createWarning( ) { return createWarning ( mNode ); } /** * Create a warning problem. This does the same as this as * createProblem plus it sets the problem object to severity warning. * * @param node * @return the problem to be recorded. */ protected IProblem createWarning ( INode node ) { IProblem problem = createProblem (node); problem.setAttribute(IProblem.SEVERITY, IProblem.SEVERITY_WARNING); return problem; } /** * Create an information problem. This does the same as this as * createProblem plus it sets the problem object to severity information. * * @param node * @return the problem to be recorded. */ protected IProblem createInfo ( INode node ) { IProblem problem = createProblem (node); problem.setAttribute(IProblem.SEVERITY, IProblem.SEVERITY_INFO); return problem; } /** * Create an information problem on the current node * * @return the problem created */ protected IProblem createInfo ( ) { return createInfo ( mNode ); } /** * Is the node defined ? The check is to see if the node is empty and is resolved. * If so, then we return true. Otherwise we return false. * * @param node * @return true if defined, false otherwise */ protected boolean isDefined ( INode node ) { return node != null && node.isResolved(); } /** * Is the node undefined ? The check is to see if the node is empty or it is unresolved. * If that's the case, we return true, otherwise we return false. * * @param node * @return true of undefined, false if defined. */ protected boolean isUndefined ( INode node ) { return node == null || node.isResolved() == false; } /** * * @param <T> * @param key * @return the value */ public <T extends Object> T getValue ( Object key ) { return (T) getValue( key , null ); } /** * @param <T> * @param key * @param def * @return the value */ public <T extends Object> T getValue ( Object key, T def ) { if (mData.containsKey(key)) { Object obj = mData.get(key); if (obj instanceof IValue) { return (T) ((IValue)obj).get(); } return (T) obj; } return def; } /** * Return the data stored under the key keyName for the node node * on its connected validator. * * @param <T> * @param node the reference node * @param key the key name * @param def the default value * @return the object stored or the default value passed */ public <T extends Object> T getValue ( INode node, Object key, T def ) { Validator validator = validatorForNode(node); if (validator != null) { return validator.getValue(key,def); } return def; } /** * @param <T> the type of the object * @param keyName the key name to use * @param value the value to set * @return the previous value under the key */ public <T extends Object> T setValue ( Object keyName, T value) { return (T) mData.put(keyName, value); } /** * @param <T> the object * @param node the node on which this value ought to be set * @param keyName the key name to use * @param value the value to set * @return the previous value held under that key or null */ public <T extends Object> T setValue ( INode node, Object keyName, T value) { Validator validator = validatorForNode(node); if (validator != null) { return validator.setValue(keyName,value); } return null; } /** * Return true if the value key is present on us * @param key * @return true if present, false if not */ public boolean containsValueKey ( String key ) { return mData.containsKey(key); } /** * Return true if the value key is contained on the node node. * @param node the node * @param key the key * @return true if value key is present, false otherwise. */ public boolean containsValueKey ( INode node, String key ) { Validator validator = validatorForNode (node); return validator != null ? validator.containsValueKey(key) : false; } Validator validatorForNode ( INode node ) { if (isDefined(node)) { return node.nodeValidator(); } return null; } /** * @return Return the first validator in the chain. */ private Validator getFirst() { Validator first = this; while (first.fPrev != null) { first = first.fPrev; } return first; } protected String toString (QName qname) { /** No namespace, just return the local part, sans the {} */ if (isEmptyOrWhitespace(qname.getNamespaceURI())) { return qname.getLocalPart(); } /** Lookup the prefix in the model query for the context node */ StringBuilder sb = new StringBuilder(32); String prefix = qname.getPrefix(); if (isEmptyOrWhitespace(prefix)) { prefix = mModelQuery.lookup(mNode, IModelQueryLookups.LOOKUP_TEXT_NS2PREFIX, qname.getNamespaceURI(), null ); } /** No prefix, then exit with the default QName */ if (prefix == null) { return qname.toString(); } return sb.append(prefix).append(":").append(qname.getLocalPart()).toString(); } /** * Returns true if the string is either null or contains just whitespace. * @param value * @return true if empty or whitespace, false otherwise. */ static public boolean isEmptyOrWhitespace( String value ) { if( value == null || value.length() == 0) { return true; } for( int i = 0, j = value.length(); i < j; i++ ) { if( ! Character.isWhitespace( value.charAt(i) ) ) { return false; } } return true; } /** * Test to see if a string is empty or has a value that is empty. * * @param value * @return true if empty or null, false otherwise. */ static public boolean isEmpty ( String value ) { return value == null || value.length() == 0; } /** Test to see if a string is non empty * * @param value the value to test * @return true if non empty, false if empty */ static public boolean isNonEmpty ( String value ) { return value != null && value.length () > 0; } }