// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.prelude.semantics.rule;
import com.yahoo.prelude.query.TermItem;
import com.yahoo.prelude.semantics.RuleBase;
import com.yahoo.prelude.semantics.engine.FlattenedItem;
import com.yahoo.prelude.semantics.engine.RuleEvaluation;
/**
* Superclass of all kinds of conditions of production rules
*
* @author bratseth
*/
public abstract class Condition {
/** The parent of this condition, or null if this is not nested */
private CompositeCondition parent = null;
/**
* The label of this condition, or null if none.
* Specified by label:condition
* The label is also the default context is no context is speficied explicitly
*/
private String label;
/**
* The name space refered by this match, or null if the default (query)
* Specified by namespace.condition in rules.
*/
private String nameSpace = null;
/**
* The name of the context created by this, or null if none
* Specified by context/condition in rules
*/
private String contextName;
/** Position constraints of the terms matched by this condition */
private Anchor anchor = Anchor.NONE;
public enum Anchor {
NONE, START, END, BOTH;
public static Anchor create(boolean start,boolean end) {
if (start && end) return Anchor.BOTH;
if (start) return Anchor.START;
if (end) return Anchor.END;
return NONE;
}
}
public Condition() {
this(null, null);
}
public Condition(String label) {
this(label, null);
}
public Condition(String label, String context) {
this.label = label;
this.contextName = context;
}
/**
* Sets the name whatever is matched by this condition can be refered as, or null
* to make it nonreferable
*/
public void setContextName(String contextName) { this.contextName = contextName; }
/**
* Returns the name whatever is matched by this condition can be referred as, or null
* if it is unreferable
*/
public String getContextName() { return contextName; }
/** Returns whether this is referable, returns context!=null by default */
protected boolean isReferable() { return contextName != null; }
/** Sets the label of this. Set to null to use the default */
public String getLabel() { return label; }
/** Returns the label of this, or null if none (the default) */
public void setLabel(String label) { this.label = label; }
/** Returns the name of the namespace of this, or null if default (query) */
public String getNameSpace() { return nameSpace; }
/** Sets the name of the namespace of this */
public void setNameSpace(String nameSpace) { this.nameSpace=nameSpace; }
/** Returns the condition this is nested within, or null if it is not nested */
public CompositeCondition getParent() { return parent; }
/** Called by CompositeCondition.addCondition() */
void setParent(CompositeCondition parent) { this.parent=parent; }
/** Sets a positional constraint on this condition */
public void setAnchor(Anchor anchor) { this.anchor = anchor; }
/** Returns the positional constraint on this anchor. This is never null */
public Anchor getAnchor() { return anchor; }
/**
* Returns whether this condition matches the given evaluation
* at the current location of the evaluation. Calls the doesMatch
* method of each condition subtype.
*/
public final boolean matches(RuleEvaluation e) {
// TODO: With this algoritm, each choice point will move to the next choice on each reevaluation
// In the case where there are multiple ellipses, we may want to do globally coordinated
// moves of all the choice points instead
try {
preMatchHook(e);
if (!matchesStartAnchor(e)) return false;
String higherLabel = e.getCurrentLabel();
if (getLabel() != null)
e.setCurrentLabel(getLabel());
boolean matches = doesMatch(e);
while ( ! matches && hasOpenChoicepoint(e)) {
matches = doesMatch(e);
}
e.setCurrentLabel(higherLabel);
if ( ! matchesEndAnchor(e)) return false;
traceResult(matches, e);
return matches;
}
finally {
postMatchHook(e);
}
}
/** Check start anchor. Trace level 4 if no match */
protected boolean matchesStartAnchor(RuleEvaluation e) {
if (anchor != Anchor.START && anchor != Anchor.BOTH) return true;
if (e.getPosition() == 0) return true;
if (e.getTraceLevel() >= 4)
e.trace(4, this + " must be at the start, which " + e.currentItem() + " isn't");
return false;
}
/** Check start anchor. Trace level 4 if no match */
protected boolean matchesEndAnchor(RuleEvaluation e) {
if (anchor != Anchor.END && anchor != Anchor.BOTH) return true;
if (e.getPosition() >= e.items().size()) return true;
if (e.getTraceLevel() >= 4)
e.trace(4, this + " must be at the end, which " + e.currentItem() + " isn't");
return false;
}
protected void traceResult(boolean matches, RuleEvaluation e) {
if (matches && e.getTraceLevel() >= 3)
e.trace(3, "Matched '" + this + "'" + getMatchInfoString(e) + " at " + e.previousItem());
if (!matches && e.getTraceLevel() >= 4)
e.trace(4, "Did not match '" + this + "' at " + e.currentItem());
}
protected String getMatchInfoString(RuleEvaluation e) {
String matchInfo = getMatchInfo(e);
if (matchInfo == null) return "";
return " as '" + matchInfo + "'";
}
/**
* Called when match is called, before anything else.
* Always call super.preMatchHook when overriding.
*/
protected void preMatchHook(RuleEvaluation e) {
e.entering(contextName);
}
/**
* Called just before match returns, on any return condition including exceptions.
* Always call super.postMatchHook when overriding
*/
protected void postMatchHook(RuleEvaluation e) {
e.leaving(contextName);
}
/**
* Override this to return a string describing what this condition has matched in this evaluation.
* Will only be called when this condition is actually matched in this condition
*
* @return info about what is matched, or null if there is no info to return (default)
*/
protected String getMatchInfo(RuleEvaluation e) { return null; }
/**
* Returns whether this condition matches the given evaluation
* at the current location of the evaluation. If there is a
* match, the evaluation must be advanced to the location beyond
* the matching item(s) before this method returns.
*/
protected abstract boolean doesMatch(RuleEvaluation e);
/**
* Returns whether there is an open choice in this or any of its subconditions.
* Returns false by default, must be overriden by conditions which may generate
* choices open accross multiple calls to matches, or contain such conditions.
*/
protected boolean hasOpenChoicepoint(RuleEvaluation e) {
return false;
}
/** Override if references needs to be set in this condition of its children */
public void makeReferences(RuleBase rules) { }
protected String getLabelString() {
if (label == null) return "";
return label + ":";
}
/** Whether the label matches the current item, true if there is no current item */
protected boolean labelMatches(RuleEvaluation e) {
FlattenedItem flattenedItem = e.currentItem();
if (flattenedItem == null) return true;
TermItem item = flattenedItem.getItem();
if (item == null) return true;
return labelMatches(item, e);
}
protected boolean labelMatches(TermItem evaluationTerm, RuleEvaluation e) {
String indexName = evaluationTerm.getIndexName();
String label = getLabel();
if (label == null)
label = e.getCurrentLabel();
if ("".equals(indexName) && label == null) return true;
if (indexName.equals(label)) return true;
if (e.getTraceLevel() >= 4)
e.trace(4, "'" + this + "' does not match, label of " + e.currentItem() + " was required to be " + label);
return false;
}
/** All instances of this produces a parseable string output */
protected abstract String toInnerString();
protected boolean isDefaultContextName() { return false; }
@Override
public String toString() {
String contextString = "";
String nameSpaceString = "";
if (contextName != null && !isDefaultContextName())
contextString = contextName + "/";
if (getNameSpace() != null)
nameSpaceString = getNameSpace() + ".";
return contextString + nameSpaceString + toInnerString();
}
}