Log in Help
Print
Homereleasesgate-5.1-beta2-build3402-ALLpluginsObsoleteMontreal_Transducersrccaumontrealiroraligatejape 〉 Constraint.java
 
/*
 *  Constraint.java - transducer class
 *
 *  Copyright (c) 1998-2001, The University of Sheffield.
 *
 *  This file is part of GATE (see http://gate.ac.uk/), and is free
 *  software, licenced under the GNU Library General Public License,
 *  Version 2, June 1991 (in the distribution as file licence.html,
 *  and also available at http://gate.ac.uk/gate/licence.html).
 *
 *  Hamish Cunningham, 24/07/98
 *
 *  Modifications by Luc Plamondon, Universit� de Montr�al, 20/11/03:
 *  - Migrated original file to the ca.umontreal.iro.rali.gate.jape package
 *  - The attributes/values are stored in a list of JdmAttribute objects,
 *    instead of a FeatureMap.  This allows us to store a comparison operator
 *    other than "equal" in addition to a feature/value pair in each 
 *    JdmAttribute object.
 *  - Removed a few constructors that were not used elsewhere.
 *  - Added a series of subsume methods (initially in gate.FeatureMap, but due 
 *    to cross-package restrictions on attributes/methods and due to speed 
 *    considerations, I did not extend a new class from SimpleFeatureMapImpl 
 *    and did not overload the subsume method).
 *
 *  $Id$
 */


package ca.umontreal.iro.rali.gate.jape;

import java.net.*;
import java.util.*;
import java.util.regex.*;

import gate.annotation.*;
import gate.creole.ontology.*;
import gate.util.*;
import gate.*;

/**
  * An individual annotation/attributes/values/operator expression. 
  * It doesn't extend PatternElement, even though it has to "match", because a 
  * set of Constraint must be applied together in order to avoid doing separate
  * selectAnnotations calls for each one.
  */
public class Constraint
implements JapeConstants, gate.creole.ANNIEConstants, java.io.Serializable, Cloneable
{
  /** Debug flag */
  private static final boolean DEBUG = false;

  /** Construction from annot type string */
  public Constraint(String annotType) {
    this.annotType = annotType;
    attributeList = new LinkedList();
  } // Construction from annot type

  /** The type of annnotation we're looking for. */
  private String annotType;

  /** Are we negated? */
  private boolean negated = false;

  /** Set negation. */
  public void negate() { negated = true; }

  /** Access to negation flag. */
  public boolean isNegated() { return negated; }

  /** Change the sign of the negation flag. */
  public void changeSign() { negated = !negated; }

  /** Get the type of annnotation we're looking for. */
  public String getAnnotType() { return annotType; }


  /** The list of attributes that must match the annotation features.  
      Attributes are JdmAttribute objects. 
  */
  private LinkedList attributeList;

  /** Get the list of attributes that must match the annotation features. 
      Attributes are JdmAttribute objects. 
  */
  public LinkedList getAttributeSeq() { return attributeList; }

  /** Get the attributes that must be present on the matched annotation. */
  public JdmAttribute[] getAttributeArray() { 
    return (JdmAttribute[]) attributeList.toArray(); 
  }

  /** Add an attribute. */
  public void addAttribute(JdmAttribute attr) {
    attributeList.add(attr);
  } // addAttribute


  /** Need cloning for processing of macro references. See comments on
    * <CODE>PatternElement.clone()</CODE>
    */
  public Object clone() {
    Constraint newC = null;
    try {
      newC = (Constraint) super.clone();
    } catch(CloneNotSupportedException e) {
      throw(new InternalError(e.toString()));
    }
    newC.annotType = annotType;
    // Shallow clone.  Is it ok?
    newC.attributeList = (LinkedList) attributeList.clone();

     return newC;
  } // clone


 /** Finish: replace dynamic data structures with Java arrays; called
    * after parsing.
    */
  public void finish() {
    /*
    if(attrs1 == null || attrs1.size() == 0) {
      attrs2 = new JdmAttribute[0];
      attrs1 = null;
      return;
    }
    int attrsLen = attrs1.size();
    attrs2 = new JdmAttribute[attrsLen];

    int i = 0;
    //for(Enumeration e = attrs1.getElements(); e.hasMoreElements(); i++) {
    //  attrs2[i] = (JdmAttribute) e.nextElement();
    //}
    Iterator iter = attrs1.keySet().iterator();
    while(iter.hasNext()) {
      String name = (String) iter.next();
      Object value = attrs1.get(name);
      attrs2[i++] = new JdmAttribute(name, value);
    }
    attrs1 = null;
    */
  } // finish

  /** Create a string representation of the object. */
  public String toString() { return toString(""); }

  /** Create a string representation of the object. */
  public String toString(String pad) {
    String newline = Strings.getNl();

    StringBuffer buf = new StringBuffer(
      pad + "Constraint: annotType(" + annotType + "); attrs(" + newline + pad
    );

    buf.append(attributeList.toString());
    buf.append(newline + pad + ") Constraint." + newline);

    return buf.toString();
  } // toString

  public String shortDesc() {
    String res = annotType + "(";
    res += attributeList.toString();
    res += ")";
    return res;
  } // shortDesc


  /** Test if an annotation complies with at least one feature and comparison 
      operators of this constraint's attributes.
      @param annot an annotation
      @return <code>true</code> if the annotation tests successfully with the 
              values and operators of at least one attribute of this constraint  and 
              <code>false</code> if not.
  */
  public boolean subsumesOne(SimpleFeatureMapImpl annot) {
    return subsumesOneOrAll(annot, false);
  }

  /** Test if an annotation complies with all features and comparison 
      operators of this constraint's attributes.
      @param annot an annotation
      @return <code>true</code> if the annotation tests successfully with the 
              values and operators from every attribute of this constraint  and 
              <code>false</code> if not.
  */
  public boolean subsumes(SimpleFeatureMapImpl annot) {
    return subsumesOneOrAll(annot, true);
  }

  /** Test if an annotation complies with the features and comparison 
      operators of this constraint's attributes.  Each feature is tested and
      then a big AND can be done if <code>anded=true</code>, or a big
      OR if <code>anded=false</code>.
      @param annot an annotation
      @param anded <code>true</code>: all features must match. 
                   <code>false</code>: at least one feature must match.
      @return <code>true</code> if the annotation tests successfully with the 
              values and operators from the attribute of this constraint  and 
              <code>false</code> if not.
  */
  protected boolean subsumesOneOrAll(SimpleFeatureMapImpl annot, boolean anded){

    boolean ored = !anded;

    // null is included in everything
    if (attributeList == null) 
      return true;

    /* General algorithm: we will now iterate over all the features to test.
       If we are ANDing them, return false as soon as a test fails.
       If we are ORing them, return true as soon as a test succeeds.
    */

    JdmAttribute attribute;

    for (ListIterator i = attributeList.listIterator(); i.hasNext(); ) {
      attribute = (JdmAttribute) i.next();
      
      String attributeName = attribute.getName();
      Object attributeValue = attribute.getValue();
      int attributeOp = attribute.getOperator();
      Object annotValue = annot.get(attributeName);

      if (attributeName == null || attributeValue == null) 
	continue;

      /* if an attribute pertains to a class from the ontology, 
	 perform ontology-aware subsume */     
      if (attributeName.equals(LOOKUP_CLASS_FEATURE_NAME) && annotValue != null) {
	
	//find the ontology url amongst the other attributes of the constraint
	Object constraintOntoObj = null;
	JdmAttribute currentAttribute = null;
	for (ListIterator j = attributeList.listIterator(); j.hasNext(); ) {
	  currentAttribute = (JdmAttribute) j.next();
	  if (currentAttribute.getName().equals(LOOKUP_ONTOLOGY_FEATURE_NAME)) {
	    constraintOntoObj = currentAttribute.getValue();
	    continue;
	  }
	}
      
	/* continue only if ont. url from constraint and annot. are the same,
	   otherwise ignore this attribute (default with true) */
	Object thisOntoObj = annot.get(LOOKUP_ONTOLOGY_FEATURE_NAME);
	if (constraintOntoObj != null && thisOntoObj != null) {
	  if (constraintOntoObj.equals(thisOntoObj)) {
	    boolean doSubsume = ontologySubsume(
						constraintOntoObj.toString(),
						attributeValue.toString(),
						annotValue.toString());
	      
	    if (attributeOp == JapeConstants.EQUAL) {
	      if (!doSubsume && anded) return false;
	      if (doSubsume && ored) return true;
	    }
	    if (attributeOp == JapeConstants.NOT_EQUAL) {
	      if (doSubsume && anded) return false;
	      if (!doSubsume && ored) return true;
	    }
	    // else: accept (ignore)
	  
	  } // if ontologies are with the same url
	} //if not null objects
      }
  
      /* process without ontology awareness */

      try {

	// EQUAL
	if (attributeOp == JapeConstants.EQUAL) {
	  boolean successful = equalSubsume(attributeName, attributeValue, annotValue);
	  if (!successful && anded) return false;
	  if (successful && ored) return true;
	} //if (attributeOp == JapeConstants.EQUAL)
	
	// NOT_EQUAL
	if (attributeOp == JapeConstants.NOT_EQUAL) {
	  boolean successful = equalSubsume(attributeName, attributeValue, annotValue);
	  if (successful && anded) return false;
	  if (!successful && ored) return true;
	} //if (attributeOp == JapeConstants.NOT_EQUAL)

	// REGEXP
	if (attributeOp == JapeConstants.REGEXP) {
	  // create a default value if annotation value is null
	  if (annotValue == null)
	    annotValue = new String();
	  boolean successful = regexpSubsume(attributeName, attributeValue, annotValue);
	  if (!successful && anded) return false;
	  if (successful && ored) return true;
	} //if (attributeOp == JapeConstants.REGEXP)
	
	// NOT_REGEXP
	if (attributeOp == JapeConstants.NOT_REGEXP) {
	  // create a default value if annotation value is null
	  if (annotValue == null)
	    annotValue = new String();
	  boolean successful = regexpSubsume(attributeName, attributeValue, annotValue);
	  if (successful && anded) return false;
	  if (!successful && ored) return true;
	} //if (attributeOp == JapeConstants.NOT_REGEXP)
	
	// GREATER
	if (attributeOp == JapeConstants.GREATER) {
	  boolean successful = greaterSubsume(attributeName, attributeValue, annotValue);
	  if (!successful && anded) return false;
	  if (successful && ored) return true;
	} //if (attributeOp == JapeConstants.GREATER)
	
	// LESSER_OR_EQUAL
	if (attributeOp == JapeConstants.LESSER_OR_EQUAL) {
	  boolean successful = greaterSubsume(attributeName, attributeValue, annotValue);
	  if (successful && anded) return false;
	  if (!successful && ored) return true;
	} //if (attributeOp == JapeConstants.GREATER)     
	
	// LESSER
	if (attributeOp == JapeConstants.LESSER) {
	  boolean successful = lesserSubsume(attributeName, attributeValue, annotValue);
	  if (!successful && anded) return false;
	  if (successful && ored) return true;
	} //if (attributeOp == JapeConstants.GREATER)
	
	// GREATER_OR_EQUAL
	if (attributeOp == JapeConstants.GREATER_OR_EQUAL) {
	  boolean successful = lesserSubsume(attributeName, attributeValue, annotValue); 
	  if (successful && anded) return false;
	  if (!successful && ored) return true;
	} //if (attributeOp == JapeConstants.GREATER)           
      }
      
      catch (JapeException je) {
	gate.util.Err.println(je.getMessage());
	if (anded) return false;
      }
      
    } // checked all attributes

    /* If all features had to match (ANDed), then we can return true because
       no test has failed so far.  If at least one feature had to match (ORed), 
       then we have to return false because no test has succeeded so far.
    */
    if (anded) 
      return true;
    else  // if (ored) 
      return false;
  
  } //subsumes()
    

  /** For a given attribute, test whether the values from the annotation and 
      from the constraint are equal.
      They must have the same type.  The method use a.equals(b) to test for 
      equality.
      @param attributeName the name of the attribute, in this constraint
      @param attributeValue the value of the attribute, in this constraint
      @param annotValue the value of the attribute, in the annotation
      
      @return true if both values are equal, false otherwise or if the values 
              have different type or if annotValue is null.
	      
      @throws JapeException if the attribute in the constraint has an unexpected
              type (other than String/Long/Double/Boolean).  This probably means
	      that the grammar parser (ParseCpsl.jj) has been changed to allow
	      that.

   */
  
  private static boolean equalSubsume(Object attributeName, Object attributeValue, 
				      Object annotValue)
    throws JapeException {

    if (annotValue == null) return false;
 
    if (attributeValue.equals(annotValue)) return true;
      
    /* The constraint's attribute can be a String/Long/Double/Boolean.
       The annotation's attribute must be of the same type,
       otherwise equals will return false.
       In that case, let's suppose the annot's attrib. is a String and
       let's try to convert it to the same type as the constraint. */
    
    if (annotValue instanceof String) {
      String annotValueString = (String) annotValue;
	
      if (attributeValue instanceof String)
	return false; // the comparison has already been made

      try {
	// the constraint's attribute is a Long
	if (attributeValue instanceof Long) {
	  if (attributeValue.equals(Long.valueOf(annotValueString)))
	    return true;
	  else 
	    return false;
	}
	// the constraint's attribute is a Double
	if (attributeValue instanceof Double){
	  if (attributeValue.equals(Double.valueOf(annotValueString)))
	    return true;
	  else
	    return false;
	}
	// the constraint's attribute is a Boolean
	if (attributeValue instanceof Boolean) {
	  if (attributeValue.equals(Boolean.valueOf(annotValueString)))
	    return true;
	  else
	    return false;
	}
   
	// if we reach that point, it means constraint has an unexpected type!
	throw new JapeException("Warning! Attribute \""+attributeName+"\" from a grammar rule has a value that is not a String/Long/Double/Boolean.  This should not be allowed."); 
      }
      catch (NumberFormatException otherType) {
	// annot is a String and cannot be converted to Long/Double/Boolean,
	// cannot be equal
	return false;
      }
    } //if String
    
    return false;
  }


  /** For a given attribute, test whether the value from the annotation matches
      the regular expression stated in the constraint.
   
      @param attributeName the name of the attribute, in this constraint
      @param attributeValue the value of the attribute, in this constraint 
             (that is, the regular expression)
      @param annotValue the value of the attribute, in the annotation

      @return true if annotValue matches the attributeValue regexp, 
              false otherwise

      @throws JapeException if the attribute in the annotation is not a String
              (pattern matching is irrelevant in that case)
	      
   */

  private static boolean regexpSubsume(Object attributeName, Object attributeValue, 
				Object annotValue)
    throws JapeException {

    String annotValueString;
    Pattern constraintPattern;

    if (annotValue instanceof String) {
      annotValueString = (String) annotValue;
      if (annotValueString == null) 
	annotValueString = new String();
      constraintPattern = (Pattern) attributeValue;
      if (constraintPattern.matcher(annotValueString).matches()) {
	return true;
      }
      else {
	return false;
      }
    }
    else {
      throw new JapeException("Cannot do pattern matching on attribute \""+attributeName.toString()+"\".  Are you sure the value is a string?");
    }
  }


  /** For a given attribute, test whether the value from the annotation is 
      greater than the value from this constraint.  This method uses the compareTo
      method from the 
      java.lang.Comparable interface to perform the test.  If any of the values
      cannot be cast to Comparable (Boolean, for example, are not Comparable),
      this method returns false.
      <p> Because sometimes numerical values are stored in annotations as
      strings (Token.length for example), this method tries to convert the
      annotation's value to a Double or a Long when the compareTo test fails,
      and then performs the compareTo test again.  

      @param attributeName the name of the attribute, in this constraint
      @param attributeValue the value of the attribute, in this constraint
      @param annotValue the value of the attribute, in the annotation

      @return true if annotValue is greater than attributeValue, 
              false otherwise or if annotValue cannot be cast to a Long/Double,
	      or if annotValue cannot be parsed to a Long/Double.

      @throws JapeException if the attribute in the annotation cannot be
              compared or parsed to an object of the same type as the attribute
              in the constraint, or if the attribute in the annotation is null.
  */

  private static boolean greaterSubsume(Object attributeName, Object attributeValue, 
				 Object annotValue)
    throws JapeException {


    if (annotValue == null)
      throw new JapeException("Cannot compare attribute \"" +
			      attributeName.toString() +
			      "\" with <, >, <= or >= because annotation value is null.");

    /* attributeValue can be a String/Long/Double/Boolean.
       First assume that annotValue is of the same type as
       attributeValue and call compareTo, except for Boolean.
       If it fails, assume that annotValue is a string then
       convert it to the same type as attributeValue and test. */

    // first check if we can call the compareTo method
    if (attributeValue instanceof Comparable) {
      Comparable attributeComparable = (Comparable) attributeValue;
      
      // then assume attribute and annot are of the same type and compare
      try {
	if (attributeComparable.compareTo(annotValue) < 0) 
	  return true;
	else
	  return false;
      }
      // attribute and annot do not have same type
      catch (ClassCastException notSameType) {

	// if annotValue is a String, there is still hope to parse it
	if (annotValue instanceof String) {
	  String annotValueString = (String) annotValue;

	  if (attributeValue instanceof String)
	    return false; // the comparison has already been made
	
	  try {
	    // if attribute is a Long, parse annot as a Long
	    if (attributeComparable instanceof Long) {
	      if (attributeComparable.compareTo(
		  Long.valueOf(annotValueString)) < 0) 
		return true;
	      else
		return false;
	    }
	    // if attribute is a Double
	    if (attributeComparable instanceof Double) {
	      if (attributeComparable.compareTo(
		  Double.valueOf(annotValueString)) < 0) 
		return true;
	      else
		return false;
	    }
	    
	    // annotValue is of another type, so cannot compare
	    return false; 
	  } //try
	  catch (NumberFormatException nfe) {
	    // attribute is a Long/Double, but annot is not: cannot compare
	    throw new JapeException("Cannot compare values for attribute \"" +
				    attributeName.toString() +
				    "\" because \"" +
				    attributeValue.toString() +
				    "\" and/or \"" +
				    annotValue.toString() +
				    "\" do not have comparable types.");
	  }
	} // if instanceof String
      } //catch notSameType
    } //if instanceof Comparable
    else {
      // cannot compare this type, so throw an exception
  
      throw new JapeException("Cannot compare values for attribute \"" +
			      attributeName.toString() +
			      "\" because \"" +
			      attributeValue.toString() +
			      "\" and/or \"" +
			      annotValue.toString() +
			      "\" do not have comparable types.");
    }
    
    return true;
  }


  /** For a given attribute, test whether the value from the annotation is 
      lesser than the value from this constraint.  This method uses the compareTo
       method from the 
      java.lang.Comparable interface to perform the test.  If any of the values
      cannot be cast to Comparable (Boolean, for example, are not Comparable), 
      this method returns false.
      <p> Because sometimes numerical values are stored in annotations as
      strings (Token.length for example), this method tries to convert the 
      annotation's value to a Double or a Long when the compareTo test fails, 
      and then performs the compareTo test again.  

      @param constraintName the name of the attribute, in the constraint
      @param attributeValue the value of the attribute, in the constraint
      @param annotValue the value of the attribute, in the annotation
      @return true if annotValue is lesser than attributeValue, 
              false otherwise or if annotValue cannot be cast to a Long/Double,
	      or if annotValue cannot be parsed to a Long/Double.

      @throws JapeException if the attribute in the annotation cannot be
              compared or parsed to an object of the same type as the attribute
              in the constraint, or if the attribute in the annotation is null.
  */

  private static boolean lesserSubsume(Object attributeName, Object attributeValue, 
				Object annotValue)
    throws JapeException {

    if (annotValue == null)
      throw new JapeException("Cannot compare attribute \"" +
			      attributeName.toString() +
			      "\" with <, >, <= or >= because annotation value is null.");

    /* attributeValue can be a String/Long/Double/Boolean.
       First assume that annotValue is of the same type as
       attributeValue and call compareTo, except for Boolean.
       If it fails, assume that annotValue is a string then
       convert it to the same type as attributeValue and test. */

    // first check if we can call the compareTo method
    if (attributeValue instanceof Comparable) {
      Comparable attributeComparable = (Comparable) attributeValue;
      
      // then assume attribute and annot are of the same type and compare
      try {
	if (attributeComparable.compareTo(annotValue) > 0) 
	  return true;
	else
	  return false;
      }
      // attribute and annot do not have same type
      catch (ClassCastException notSameType) {

	// if annotValue is a String, there is still hope to parse it
	if (annotValue instanceof String) {
	  String annotValueString = (String) annotValue;

	  if (attributeValue instanceof String)
	    return false; // the comparison has already been made
	
	  try {
	    // if attribute is a Long, parse annot as a Long
	    if (attributeComparable instanceof Long) {
	      if (attributeComparable.compareTo(
		  Long.valueOf(annotValueString)) > 0) 
		return true;
	      else
		return false;
	    }
	    // if attribute is a Double
	    if (attributeComparable instanceof Double) {
	      if (attributeComparable.compareTo(
		  Double.valueOf(annotValueString)) > 0) 
		return true;
	      else
		return false;
	    }
	    
	    // annotValue is of another type, so cannot compare
	    return false; 
	  } //try
	  catch (NumberFormatException nfe) {
	    // attribute is a Long/Double, but annot is not: cannot compare
	    throw new JapeException("Cannot compare values for attribute \"" +
				    attributeName.toString() +
				    "\" because \"" +
				    attributeValue.toString() +
				    "\" and/or \"" +
				    annotValue.toString() +
				    "\" do not have comparable types.");
	  }
	} // if instanceof String
      } //catch notSameType
    } //if instanceof Comparable
    else {
      // cannot compare this type, so throw an exception
  
      throw new JapeException("Cannot compare values for attribute \"" +
			      attributeName.toString() +
			      "\" because \"" +
			      attributeValue.toString() +
			      "\" and/or \"" +
			      annotValue.toString() +
			      "\" do not have comparable types.");
    }
    
    return true;
  }


  /**ontology enhanced subsume
   * @param ontoUrl the url of the ontology to be used
   * @return true if value1 subsumes value2 in the specified ontology */
  private static boolean ontologySubsume(String ontoUrl,String value1,String value2) {

    // this method is a copy of gate.util.SimpleFeatureMapImpl.ontologySubsume

    boolean result = false;
    try {
      URL url;
      try {
        url = new URL(ontoUrl);
      } catch (MalformedURLException e){
        throw new RuntimeException(
        "\nin SimpleFeatureMapImpl on ontologySubsume()\n"
        +e.getMessage()+"\n");
      }

      /* GET ONTOLOGY BY URL : a bit tricky reference
      since the behaviour behind the getOntology method is
      certainly static.
      : should be temporary */
      Ontology o = OntologyUtilities.getOntology(url);

      OClass c1 = (OClass) o.getOResourceByName(value1);
      OClass c2 = (OClass) o.getOResourceByName(value2);

      if (null!= c1 && null!= c2) {
        if (c1.equals(c2)) {
          result = true;
        } else {
          Set subs1;
          subs1 = c1.getSubClasses(OConstants.TRANSITIVE_CLOSURE);
          if (subs1.contains(c2))
            result = true;
        } // else
      } // if not null classes
    } catch  (gate.creole.ResourceInstantiationException x) {
      x.printStackTrace(Err.getPrintWriter());
    }
    return result;
  } // ontologySubsume

} // class Constraint