org.modeshape.jcr.query.model.FullTextSearch Maven / Gradle / Ivy
/*
* ModeShape (http://www.modeshape.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.modeshape.jcr.query.model;
import java.io.InputStream;
import java.math.BigDecimal;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.Calendar;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;
import javax.jcr.Binary;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.ValueFormatException;
import org.modeshape.common.annotation.Immutable;
import org.modeshape.common.text.ParsingException;
import org.modeshape.common.util.CheckArg;
import org.modeshape.common.util.HashCode;
import org.modeshape.common.util.ObjectUtil;
import org.modeshape.jcr.query.engine.ScanningQueryEngine;
import org.modeshape.jcr.query.parse.FullTextSearchParser;
/**
* A constraint that evaluates to true only when a full-text search applied to the search scope results in positive findings. If a
* property name is supplied, then the search is limited to the value(s) of the named property on the node(s) in the search scope.
*/
@Immutable
public class FullTextSearch implements Constraint, javax.jcr.query.qom.FullTextSearch {
private static final long serialVersionUID = 1L;
protected static String toString( StaticOperand operand ) throws RepositoryException {
if (operand instanceof javax.jcr.query.qom.Literal) {
return ((javax.jcr.query.qom.Literal)operand).getLiteralValue().getString();
}
return operand.toString();
}
private final SelectorName selectorName;
private final String propertyName;
private final String fullTextSearchExpression;
private Term term;
private final int hc;
private transient StaticOperand expression;
/**
* Create a constraint defining a full-text search against the property values on node within the search scope.
*
* @param selectorName the name of the node selector defining the search scope
* @param propertyName the name of the property to be searched; may be null if all property values are to be searched
* @param fullTextSearchExpression the search expression
* @param term the term representation, if it is known; may be null
* @throws RepositoryException if there is an error converting the full text search expression to a string
*/
public FullTextSearch( SelectorName selectorName,
String propertyName,
StaticOperand fullTextSearchExpression,
Term term ) throws RepositoryException {
CheckArg.isNotNull(selectorName, "selectorName");
CheckArg.isNotNull(fullTextSearchExpression, "fullTextSearchExpression");
String expressionString = toString(fullTextSearchExpression);
CheckArg.isNotEmpty(expressionString, "fullTextSearchExpression");
this.selectorName = selectorName;
this.propertyName = propertyName;
this.term = term;
this.fullTextSearchExpression = expressionString;
this.hc = HashCode.compute(this.selectorName, this.propertyName, this.fullTextSearchExpression);
this.expression = fullTextSearchExpression;
}
/**
* Create a constraint defining a full-text search against the property values on node within the search scope.
*
* @param selectorName the name of the node selector defining the search scope
* @param propertyName the name of the property to be searched; may be null if all property values are to be searched
* @param expressionString the string form of the full text search expression; may not be null or empty
* @param fullTextSearchExpression the search expression
*/
public FullTextSearch( SelectorName selectorName,
String propertyName,
String expressionString,
StaticOperand fullTextSearchExpression) {
CheckArg.isNotNull(selectorName, "selectorName");
CheckArg.isNotNull(fullTextSearchExpression, "fullTextSearchExpression");
CheckArg.isNotEmpty(expressionString, "expressionString");
this.selectorName = selectorName;
this.propertyName = propertyName;
this.fullTextSearchExpression = expressionString;
this.hc = HashCode.compute(this.selectorName, this.propertyName, this.fullTextSearchExpression);
this.expression = fullTextSearchExpression;
}
/**
* Create a constraint defining a full-text search against the property values on node within the search scope.
*
* @param selectorName the name of the node selector defining the search scope
* @param propertyName the name of the property to be searched; may be null if all property values are to be searched
* @param fullTextSearchExpression the search expression
* @param term the term representation, if it is known; may be null
*/
public FullTextSearch( SelectorName selectorName,
String propertyName,
String fullTextSearchExpression,
Term term ) {
CheckArg.isNotNull(selectorName, "selectorName");
CheckArg.isNotEmpty(fullTextSearchExpression, "fullTextSearchExpression");
this.selectorName = selectorName;
this.propertyName = propertyName;
this.term = term;
this.fullTextSearchExpression = fullTextSearchExpression;
this.hc = HashCode.compute(this.selectorName, this.propertyName, this.fullTextSearchExpression);
}
/**
* Create a constraint defining a full-text search against the property values on node within the search scope.
*
* @param selectorName the name of the node selector defining the search scope
* @param propertyName the name of the property to be searched; may be null if all property values are to be searched
* @param fullTextSearchExpression the search expression
*/
public FullTextSearch( SelectorName selectorName,
String propertyName,
String fullTextSearchExpression ) {
CheckArg.isNotNull(selectorName, "selectorName");
CheckArg.isNotEmpty(fullTextSearchExpression, "fullTextSearchExpression");
this.selectorName = selectorName;
this.propertyName = propertyName;
this.fullTextSearchExpression = fullTextSearchExpression;
this.term = null;
this.hc = HashCode.compute(this.selectorName, this.propertyName, this.fullTextSearchExpression);
}
/**
* Create a constraint defining a full-text search against the node within the search scope.
*
* @param selectorName the name of the node selector defining the search scope
* @param fullTextSearchExpression the search expression
*/
public FullTextSearch( SelectorName selectorName,
String fullTextSearchExpression ) {
CheckArg.isNotNull(selectorName, "selectorName");
CheckArg.isNotEmpty(fullTextSearchExpression, "fullTextSearchExpression");
this.selectorName = selectorName;
this.propertyName = null;
this.term = null;
this.fullTextSearchExpression = fullTextSearchExpression;
this.hc = HashCode.compute(this.selectorName, this.propertyName, this.fullTextSearchExpression);
}
/**
* Get the name of the selector that is to be searched
*
* @return the selector name; never null
*/
public final SelectorName selectorName() {
return selectorName;
}
@Override
public String getSelectorName() {
return selectorName.getString();
}
@Override
public final String getPropertyName() {
return propertyName;
}
/**
* Get the full-text search expression, as a string.
*
* @return the search expression; never null
*/
public final String fullTextSearchExpression() {
return fullTextSearchExpression;
}
@Override
public StaticOperand getFullTextSearchExpression() {
if (expression == null) {
// This is idempotent, so we don't need to worry about concurrently setting the value ...
this.expression = new Literal(new Value() {
@Override
public int getType() {
return PropertyType.STRING;
}
@Override
public String getString() {
return fullTextSearchExpression();
}
@Override
public InputStream getStream() throws RepositoryException {
throw new ValueFormatException();
}
@Override
public long getLong() throws ValueFormatException, RepositoryException {
throw new ValueFormatException();
}
@Override
public double getDouble() throws ValueFormatException, RepositoryException {
throw new ValueFormatException();
}
@Override
public BigDecimal getDecimal() throws ValueFormatException, RepositoryException {
throw new ValueFormatException();
}
@Override
public Calendar getDate() throws ValueFormatException, RepositoryException {
throw new ValueFormatException();
}
@Override
public boolean getBoolean() throws ValueFormatException, RepositoryException {
throw new ValueFormatException();
}
@Override
public Binary getBinary() throws RepositoryException {
throw new ValueFormatException();
}
});
}
return expression;
}
/**
* Get the formal {@link Term} representation of the expression.
*
* @return the term representing this search; never null
* @throws ParsingException if there is an error producing the term representation
*/
public Term getTerm() {
// Idempotent, so okay to not lock/synchronize ...
if (term == null) {
term = new FullTextSearchParser().parse(fullTextSearchExpression);
}
return term;
}
@Override
public String toString() {
return Visitors.readable(this);
}
@Override
public int hashCode() {
return hc;
}
@Override
public boolean equals( Object obj ) {
if (obj == this) return true;
if (obj instanceof FullTextSearch) {
FullTextSearch that = (FullTextSearch)obj;
if (this.hc != that.hc) return false;
if (!this.selectorName.equals(that.selectorName)) return false;
if (!ObjectUtil.isEqualWithNulls(this.propertyName, that.propertyName)) return false;
if (!this.fullTextSearchExpression.equals(that.fullTextSearchExpression)) return false;
return true;
}
return false;
}
public FullTextSearch withFullTextExpression( String expression ) {
return new FullTextSearch(selectorName, propertyName, expression);
}
@Override
public void accept( Visitor visitor ) {
visitor.visit(this);
}
/**
* The general notion of a term that makes up a full-text search.
*/
public static interface Term {
/**
* Checks if the term matches (from a FTS perspective) the given value.
*
* @param value a non-null string
* @return {@code true} if the term matches the value, {@code false} otherwise
*/
public boolean matches(String value);
}
/**
* A {@link Term} that represents a search term that requires another term to not appear.
*/
public static class NegationTerm implements Term {
private final Term negated;
public NegationTerm( Term negatedTerm ) {
assert negatedTerm != null;
this.negated = negatedTerm;
}
/**
* Get the term that is negated.
*
* @return the negated term; never null
*/
public Term getNegatedTerm() {
return negated;
}
@Override
public boolean matches( String value ) {
return !negated.matches(value);
}
@Override
public int hashCode() {
return negated.hashCode();
}
@Override
public boolean equals( Object obj ) {
if (obj == this) return true;
if (obj instanceof NegationTerm) {
NegationTerm that = (NegationTerm)obj;
return this.getNegatedTerm().equals(that.getNegatedTerm());
}
return false;
}
@Override
public String toString() {
return "-" + negated.toString();
}
}
/**
* A {@link Term} that represents a single search term. The term may be comprised of multiple words.
*/
public static class SimpleTerm implements Term {
private final String value;
private final boolean quoted;
private final Pattern pattern;
/**
* Create a simple term with the value and whether the term is excluded or included.
*
* @param value the value that makes up the term
*/
public SimpleTerm( String value ) {
assert value != null;
assert value.trim().length() > 0;
this.value = value;
this.quoted = this.value.indexOf(' ') != -1;
this.pattern = Pattern.compile(regexFromValue(), Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
}
private String regexFromValue() {
String value = this.value;
//parse a LIKE-style expression around the value which should ensure that any JCR wildcards are converted
//to regex wildcards
if (!value.startsWith("%") && !value.startsWith("*")) {
value = "%" + value;
}
if (!value.endsWith("%") && !value.endsWith("*")) {
value = value + "%";
}
return ScanningQueryEngine.toRegularExpression(value);
}
/**
* Get the value of this term. Note that this is the actual value that is to be searched for, and will not include the
* {@link #isQuotingRequired() quotes}.
*
* @return the value; never null
*/
public String getValue() {
return value;
}
/**
* Get the values of this term if the term is quoted.
*
* @return the array of terms; never null
*/
public String[] getValues() {
return value.split("/w");
}
/**
* Get whether this term needs to be quoted because it consists of multiple words.
*
* @return true if the term needs to be quoted, or false otherwise
*/
public boolean isQuotingRequired() {
return quoted;
}
/**
* Return whether this term contains any unescaped wildcard characters (e.g., one of '*', '?', '%', or '_').
*
* @return true if this term contains unescaped wildcard characters, or false otherwise
*/
public boolean containsWildcards() {
if (this.value.length() == 0) return false;
CharacterIterator iter = new StringCharacterIterator(this.value);
boolean skipNext = false;
for (char c = iter.first(); c != CharacterIterator.DONE; c = iter.next()) {
if (skipNext) {
skipNext = false;
continue;
}
if (c == '*' || c == '?' || c == '%' || c == '_') return true;
if (c == '\\') skipNext = true;
}
return false;
}
@Override
public boolean matches( String value ) {
return pattern.matcher(value).matches();
}
@Override
public int hashCode() {
return value.hashCode();
}
@Override
public boolean equals( Object obj ) {
if (obj == this) return true;
if (obj instanceof SimpleTerm) {
SimpleTerm that = (SimpleTerm)obj;
return this.getValue().equals(that.getValue());
}
return false;
}
@Override
public String toString() {
return quoted ? "\"" + this.value + "\"" : this.value;
}
}
/**
* A list of {@link Term}s.
*/
public static abstract class CompoundTerm implements Term, Iterable {
private final List terms;
/**
* Create a compound term of the supplied terms.
*
* @param terms the terms; may not be null or empty
*/
protected CompoundTerm( List terms ) {
this.terms = terms;
}
/**
* Get the terms that make up this compound term.
*
* @return the terms in the disjunction; never null and never empty
*/
public List getTerms() {
return terms;
}
@Override
public Iterator iterator() {
return terms.iterator();
}
@Override
public int hashCode() {
return terms.hashCode();
}
@Override
public boolean equals( Object obj ) {
if (obj == this) return true;
if (this.getClass().isInstance(obj)) {
CompoundTerm that = (CompoundTerm)obj;
return this.getTerms().equals(that.getTerms());
}
return false;
}
protected String toString( String delimiter ) {
if (terms.size() == 1) return terms.iterator().next().toString();
StringBuilder sb = new StringBuilder();
sb.append("( ");
boolean first = true;
for (Term term : terms) {
if (first) first = false;
else sb.append(' ').append(delimiter).append(' ');
sb.append(term);
}
sb.append(" )");
return sb.toString();
}
}
/**
* A set of {@link Term}s that are ORed together.
*/
public static class Disjunction extends CompoundTerm {
/**
* Create a disjunction of the supplied terms.
*
* @param terms the terms to be ORed together; may not be null or empty
*/
public Disjunction( List terms ) {
super(terms);
}
@Override
public boolean matches( String value ) {
for (Term term : getTerms()) {
if (term.matches(value)) {
return true;
}
}
return false;
}
@Override
public String toString() {
return toString("OR");
}
}
/**
* A set of {@link Term}s that are ANDed together.
*/
public static class Conjunction extends CompoundTerm {
/**
* Create a conjunction of the supplied terms.
*
* @param terms the terms to be ANDed together; may not be null or empty
*/
public Conjunction( List terms ) {
super(terms);
}
@Override
public boolean matches( String value ) {
for (Term term : getTerms()) {
if (!term.matches(value)) {
return false;
}
}
return true;
}
@Override
public String toString() {
return toString("AND");
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy