/*
* 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;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import javax.jcr.Node;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.nodetype.PropertyDefinition;
import org.modeshape.common.annotation.Immutable;
import org.modeshape.jcr.api.query.qom.Operator;
import org.modeshape.jcr.api.query.qom.QueryObjectModelConstants;
import org.modeshape.jcr.api.value.DateTime;
import org.modeshape.jcr.cache.NodeKey;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.NameFactory;
import org.modeshape.jcr.value.NamespaceRegistry;
import org.modeshape.jcr.value.Path;
import org.modeshape.jcr.value.PathFactory;
import org.modeshape.jcr.value.Property;
import org.modeshape.jcr.value.StringFactory;
import org.modeshape.jcr.value.ValueFactories;
import org.modeshape.jcr.value.ValueFactory;
import org.modeshape.jcr.value.ValueFormatException;
/**
* ModeShape implementation of the {@link PropertyDefinition} interface. This implementation is immutable and has all fields
* initialized through its constructor.
*/
@Immutable
class JcrPropertyDefinition extends JcrItemDefinition implements PropertyDefinition {
protected static final Map<String, Operator> OPERATORS_BY_JCR_NAME;
static {
Map<String, Operator> map = new HashMap<>();
map.put(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, Operator.EQUAL_TO);
map.put(QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN, Operator.GREATER_THAN);
map.put(QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO, Operator.GREATER_THAN_OR_EQUAL_TO);
map.put(QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN, Operator.LESS_THAN);
map.put(QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO, Operator.LESS_THAN_OR_EQUAL_TO);
map.put(QueryObjectModelConstants.JCR_OPERATOR_LIKE, Operator.LIKE);
map.put(QueryObjectModelConstants.JCR_OPERATOR_NOT_EQUAL_TO, Operator.NOT_EQUAL_TO);
OPERATORS_BY_JCR_NAME = Collections.unmodifiableMap(map);
}
static Operator operatorFromSymbol( String jcrConstantValue ) {
Operator op = OPERATORS_BY_JCR_NAME.get(jcrConstantValue);
if (op == null) op = Operator.forSymbol(jcrConstantValue);
assert op != null;
return op;
}
private final Object[] rawDefaultValues;
private final JcrValue[] defaultValues;
private final int requiredType;
private final String[] valueConstraints;
private final boolean multiple;
private final boolean fullTextSearchable;
private final boolean queryOrderable;
private final String[] queryOperators;
private final NodeKey key;
private final PropertyDefinitionId id;
private ConstraintChecker checker = null;
JcrPropertyDefinition( ExecutionContext context,
JcrNodeType declaringNodeType,
NodeKey prototypeKey,
Name name,
int onParentVersion,
boolean autoCreated,
boolean mandatory,
boolean protectedItem,
JcrValue[] defaultValues,
int requiredType,
String[] valueConstraints,
boolean multiple,
boolean fullTextSearchable,
boolean queryOrderable,
String[] queryOperators ) {
super(context, declaringNodeType, name, onParentVersion, autoCreated, mandatory, protectedItem);
this.defaultValues = defaultValues;
this.requiredType = requiredType;
this.valueConstraints = valueConstraints;
assert this.valueConstraints != null;
if (requiredType != PropertyType.UNDEFINED && valueConstraints.length > 0) {
// if we have a required type, create the default checker eagerly to detect any invalid constraint values
this.checker = createChecker(context, requiredType, valueConstraints);
}
this.multiple = multiple;
this.fullTextSearchable = fullTextSearchable;
this.queryOrderable = queryOrderable;
this.queryOperators = queryOperators != null ? queryOperators : new String[] {
QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN,
QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO, QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN,
QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO, QueryObjectModelConstants.JCR_OPERATOR_LIKE,
QueryObjectModelConstants.JCR_OPERATOR_NOT_EQUAL_TO};
this.id = this.declaringNodeType == null ? null : new PropertyDefinitionId(this.declaringNodeType.getInternalName(),
this.name, this.requiredType, this.multiple);
this.key = this.id == null ? prototypeKey : prototypeKey.withId("/jcr:system/jcr:nodeTypes/" + this.id.getString());
if (this.defaultValues != null) {
this.rawDefaultValues = new Object[this.defaultValues.length];
int i = 0;
for (JcrValue defaultValue : this.defaultValues) {
rawDefaultValues[i++] = defaultValue.value();
}
} else {
this.rawDefaultValues = null;
}
}
/**
* Get the durable identifier for this property definition.
*
* @return the property definition ID; never null
*/
public PropertyDefinitionId getId() {
return id;
}
@Override
final NodeKey key() {
return key;
}
@Override
public JcrValue[] getDefaultValues() {
return defaultValues;
}
/**
* Get the default values array consisting of values that can be placed inside {@link Property} instances.
*
* @return the default values, or null if there are none
*/
Object[] getRawDefaultValues() {
return rawDefaultValues;
}
/**
* Return whether this definition has default values.
*
* @return true if there default values, or false otherwise
*/
public boolean hasDefaultValues() {
return defaultValues != null;
}
@Override
public int getRequiredType() {
return requiredType;
}
@Override
public String[] getValueConstraints() {
return valueConstraints;
}
@Override
public boolean isMultiple() {
return multiple;
}
@Override
public boolean isFullTextSearchable() {
return fullTextSearchable;
}
@Override
public boolean isQueryOrderable() {
return queryOrderable;
}
@Override
public String[] getAvailableQueryOperators() {
return queryOperators;
}
/**
* Creates a new <code>JcrPropertyDefinition</code> that is identical to the current object, but with the given
* <code>declaringNodeType</code>. Provided to support immutable pattern for this class.
*
* @param declaringNodeType the declaring node type for the new <code>JcrPropertyDefinition</code>
* @return a new <code>JcrPropertyDefinition</code> that is identical to the current object, but with the given
* <code>declaringNodeType</code>.
*/
JcrPropertyDefinition with( JcrNodeType declaringNodeType ) {
return new JcrPropertyDefinition(this.context, declaringNodeType, key(), this.name, this.getOnParentVersion(),
this.isAutoCreated(), this.isMandatory(), this.isProtected(), this.getDefaultValues(),
this.getRequiredType(), this.getValueConstraints(), this.isMultiple(),
this.isFullTextSearchable(), this.isQueryOrderable(), this.getAvailableQueryOperators());
}
/**
* Creates a new <code>JcrPropertyDefinition</code> that is identical to the current object, but with the given
* <code>context</code>. Provided to support immutable pattern for this class.
*
* @param context the {@link ExecutionContext} for the new <code>JcrPropertyDefinition</code>
* @return a new <code>JcrPropertyDefinition</code> that is identical to the current object, but with the given
* <code>context</code>.
*/
JcrPropertyDefinition with( ExecutionContext context ) {
return new JcrPropertyDefinition(context, this.declaringNodeType, key(), this.name, this.getOnParentVersion(),
this.isAutoCreated(), this.isMandatory(), this.isProtected(), this.getDefaultValues(),
this.getRequiredType(), this.getValueConstraints(), this.isMultiple(),
this.isFullTextSearchable(), this.isQueryOrderable(), this.getAvailableQueryOperators());
}
@Override
public String toString() {
ValueFactory<String> strings = context.getValueFactories().getStringFactory();
StringBuilder sb = new StringBuilder();
PropertyDefinitionId id = getId();
sb.append(strings.create(id.getNodeTypeName()));
sb.append('/');
sb.append(strings.create(id.getPropertyDefinitionName()));
sb.append('/');
sb.append(org.modeshape.jcr.api.PropertyType.nameFromValue(id.getPropertyType()));
sb.append(id.allowsMultiple() ? '*' : '1');
return sb.toString();
}
boolean satisfiesConstraints( Value value,
JcrSession session ) {
if (value == null) return false;
if (valueConstraints == null || valueConstraints.length == 0) {
return true;
}
// Neither the 1.0 or 2.0 specification formally prohibit constraints on properties with no required type.
int type = requiredType == PropertyType.UNDEFINED ? value.getType() : requiredType;
/*
* Keep a method-local reference to the constraint checker in case another thread attempts to concurrently
* check the constraints with a different required type.
*/
ConstraintChecker checker = this.checker;
if (checker == null || checker.getType() != type) {
checker = createChecker(context, type, valueConstraints);
this.checker = checker;
}
try {
return checker.matches(value, session);
} catch (ValueFormatException vfe) {
// The value was so wonky that we couldn't even convert it to an appropriate type
return false;
}
}
boolean satisfiesConstraints( Value[] values,
JcrSession session ) {
if (valueConstraints == null || valueConstraints.length == 0) {
if (requiredType != PropertyType.UNDEFINED) {
for (Value value : values) {
if (value.getType() != requiredType) return false;
}
}
return true;
}
if (values == null || values.length == 0) {
// There are no values, so see if the definition allows multiple values ...
return isMultiple();
}
// Neither the 1.0 or 2.0 specification formally prohibit constraints on properties with no required type.
int type = requiredType == PropertyType.UNDEFINED ? values[0].getType() : requiredType;
/*
* Keep a method-local reference to the constraint checker in case another thread attempts to concurrently
* check the constraints with a different required type.
*/
ConstraintChecker checker = this.checker;
if (checker == null || checker.getType() != type) {
checker = createChecker(context, type, valueConstraints);
this.checker = checker;
}
try {
for (Value value : values) {
if (requiredType != PropertyType.UNDEFINED && value.getType() != requiredType) return false;
if (!checker.matches(value, session)) return false;
}
return true;
} catch (ValueFormatException vfe) {
// The value was so wonky that we couldn't even convert it to an appropriate type
return false;
}
}
/**
* Return the minimum value allowed by the constraints, or null if no such minimum value is defined by the definition given
* it's required type and constraints. A minimum value can only be found for numeric types, such as {@link PropertyType#DATE
* DATE}, {@link PropertyType#LONG LONG}, {@link PropertyType#DOUBLE DOUBLE}, and {@link PropertyType#DECIMAL DECIMAL}; all
* other types will return null.
*
* @return the minimum value, or null if no minimum value could be identified
*/
Object getMinimumValue() {
if (requiredType == PropertyType.DATE || requiredType == PropertyType.DOUBLE || requiredType == PropertyType.LONG
|| requiredType == PropertyType.DECIMAL) {
ConstraintChecker checker = this.checker;
if (checker == null || checker.getType() != requiredType) {
checker = createChecker(context, requiredType, valueConstraints);
this.checker = checker;
}
assert checker instanceof RangeConstraintChecker;
RangeConstraintChecker<?> rangeChecker = (RangeConstraintChecker<?>)checker;
return rangeChecker.getMinimum(); // may still be null
}
return null;
}
/**
* Return the maximum value allowed by the constraints, or null if no such maximum value is defined by the definition given
* it's required type and constraints. A maximum value can only be found for numeric types, such as {@link PropertyType#DATE
* DATE}, {@link PropertyType#LONG LONG}, {@link PropertyType#DOUBLE DOUBLE}, and {@link PropertyType#DECIMAL DECIMAL}; all
* other types will return null.
*
* @return the maximum value, or null if no maximum value could be identified
*/
Object getMaximumValue() {
if (requiredType == PropertyType.DATE || requiredType == PropertyType.DOUBLE || requiredType == PropertyType.LONG
|| requiredType == PropertyType.DECIMAL) {
ConstraintChecker checker = this.checker;
if (checker == null || checker.getType() != requiredType) {
checker = createChecker(context, requiredType, valueConstraints);
this.checker = checker;
}
assert checker instanceof RangeConstraintChecker;
RangeConstraintChecker<?> rangeChecker = (RangeConstraintChecker<?>)checker;
return rangeChecker.getMaximum(); // may still be null
}
return null;
}
/**
* Returns <code>true</code> if <code>value</code> can be cast to <code>property.getRequiredType()</code> per the type
* conversion rules in section 3.6.4 of the JCR 2.0 specification. If the property definition has a required type of
* {@link PropertyType#UNDEFINED}, the cast will be considered to have succeeded.
*
* @param value the value to be validated
* @return <code>true</code> if the value can be cast to the required type for the property definition (if it exists).
*/
boolean canCastToType( Value value ) {
try {
assert value instanceof JcrValue : "Illegal implementation of Value interface";
((JcrValue)value).asType(getRequiredType()); // throws ValueFormatException if there's a problem
return true;
} catch (javax.jcr.ValueFormatException vfe) {
// Cast failed
return false;
}
}
/**
* Returns <code>true</code> if <code>value</code> can be cast to <code>property.getRequiredType()</code> per the type
* conversion rules in section 3.6.4 of the JCR 2.0 specification. If the property definition has a required type of
* {@link PropertyType#UNDEFINED}, the cast will be considered to have succeeded.
*
* @param values the values to be validated
* @return <code>true</code> if the value can be cast to the required type for the property definition (if it exists).
*/
boolean canCastToType( Value[] values ) {
for (Value value : values) {
if (!canCastToType(value)) return false;
}
return true;
}
/**
* Returns <code>true</code> if <code>value</code> can be cast to <code>property.getRequiredType()</code> per the type
* conversion rules in section 3.6.4 of the JCR 2.0 specification AND <code>value</code> satisfies the constraints (if any)
* for the property definition. If the property definition has a required type of {@link PropertyType#UNDEFINED}, the cast
* will be considered to have succeeded and the value constraints (if any) will be interpreted using the semantics for the
* type specified in <code>value.getType()</code>.
*
* @param value the value to be validated
* @param session the session in which the constraints are to be checked; may not be null
* @return <code>true</code> if the value can be cast to the required type for the property definition (if it exists) and
* satisfies the constraints for the property (if any exist).
* @see PropertyDefinition#getValueConstraints()
* @see #satisfiesConstraints(Value,JcrSession)
*/
boolean canCastToTypeAndSatisfyConstraints( Value value,
JcrSession session ) {
try {
assert value instanceof JcrValue : "Illegal implementation of Value interface";
((JcrValue)value).asType(getRequiredType()); // throws ValueFormatException if there's a problem
return satisfiesConstraints(value, session);
} catch (javax.jcr.ValueFormatException | org.modeshape.jcr.value.ValueFormatException vfe) {
// Cast failed
return false;
}
}
/**
* Returns <code>true</code> if <code>value</code> can be cast to <code>property.getRequiredType()</code> per the type
* conversion rules in section 3.6.4 of the JCR 2.0 specification AND <code>value</code> satisfies the constraints (if any)
* for the property definition. If the property definition has a required type of {@link PropertyType#UNDEFINED}, the cast
* will be considered to have succeeded and the value constraints (if any) will be interpreted using the semantics for the
* type specified in <code>value.getType()</code>.
*
* @param values the values to be validated
* @param session the session in which the constraints are to be checked; may not be null
* @return <code>true</code> if the value can be cast to the required type for the property definition (if it exists) and
* satisfies the constraints for the property (if any exist).
* @see PropertyDefinition#getValueConstraints()
* @see #satisfiesConstraints(Value,JcrSession)
*/
boolean canCastToTypeAndSatisfyConstraints( Value[] values,
JcrSession session ) {
for (Value value : values) {
if (!canCastToTypeAndSatisfyConstraints(value, session)) return false;
}
return true;
}
/**
* Returns a {@link ConstraintChecker} that will interpret the constraints described by <code>valueConstraints</code> using
* the semantics defined in section 3.6.4 of the JCR 2.0 specification for the type indicated by <code>type</code> (where
* <code>type</code> is a value from {@link PropertyType}) for the given <code>context</code>. The {@link ExecutionContext} is
* used to provide namespace mappings and value factories for the other constraint checkers.
*
* @param context the execution context
* @param type the type of constraint checker that should be created (based on values from {@link PropertyType}).
* Type-specific semantics are defined in section 3.7.3.6 of the JCR 2.0 specification.
* @param valueConstraints the constraints for the node as provided by {@link PropertyDefinition#getValueConstraints()}.
* @return a constraint checker that matches the given parameters
*/
private ConstraintChecker createChecker( ExecutionContext context,
int type,
String[] valueConstraints ) {
switch (type) {
case PropertyType.BINARY:
return new BinaryConstraintChecker(valueConstraints, context);
case PropertyType.DATE:
return new DateTimeConstraintChecker(valueConstraints, context);
case PropertyType.DOUBLE:
return new DoubleConstraintChecker(valueConstraints, context);
case PropertyType.LONG:
return new LongConstraintChecker(valueConstraints, context);
case PropertyType.NAME:
return new NameConstraintChecker(valueConstraints, context);
case PropertyType.PATH:
return new PathConstraintChecker(valueConstraints, context);
case PropertyType.REFERENCE:
case PropertyType.WEAKREFERENCE:
return new ReferenceConstraintChecker(valueConstraints, context);
case org.modeshape.jcr.api.PropertyType.SIMPLE_REFERENCE:
return new SimpleReferenceConstraintChecker(valueConstraints, context);
case PropertyType.URI:
case PropertyType.STRING:
return new StringConstraintChecker(valueConstraints, context);
case PropertyType.DECIMAL:
return new DecimalConstraintChecker(valueConstraints, context);
case PropertyType.BOOLEAN: {
return new BooleanConstraintChecker(context, valueConstraints);
}
default:
throw new IllegalStateException("Invalid property type: " + type);
}
}
/**
* Determine if the constraints on this definition are as-constrained or more-constrained than those on the supplied
* definition.
*
* @param other the property definition to compare; may not be null
* @param context the execution context used to parse any values within the constraints
* @return true if this property definition is as-constrained or more-constrained, or false otherwise
*/
boolean isAsOrMoreConstrainedThan( PropertyDefinition other,
ExecutionContext context ) {
String[] otherConstraints = other.getValueConstraints();
if (otherConstraints == null || otherConstraints.length == 0) {
// The ancestor's definition is less constrained, so it's okay even if this definition has no constraints ...
return true;
}
String[] constraints = this.getValueConstraints();
if (constraints == null || constraints.length == 0) {
// This definition has no constraints, while the ancestor does have them ...
return false;
}
// There are constraints on both, so make sure they have the same types ...
int type = this.getRequiredType();
int otherType = other.getRequiredType();
if (type == otherType && type != PropertyType.UNDEFINED) {
ConstraintChecker thisChecker = createChecker(context, type, constraints);
ConstraintChecker thatChecker = createChecker(context, otherType, otherConstraints);
return thisChecker.isAsOrMoreConstrainedThan(thatChecker);
}
// We can only compare constraint literals, and we can only expect that every constraint literal in this
// definition can be found in the other defintion (which can have more than this one) ...
Set<String> thatLiterals = new HashSet<String>();
for (String literal : otherConstraints) {
thatLiterals.add(literal);
}
for (String literal : constraints) {
if (!thatLiterals.contains(literal)) return false;
}
return true;
}
/**
* Get a constraint checker that can be used to compare constraints.
*
* @param context the execution context; may not be null
* @return the constraint checker; never null
*/
ConstraintChecker getConstraintChecker( ExecutionContext context ) {
return createChecker(context, getRequiredType(), getValueConstraints());
}
@Override
public int hashCode() {
return getId().toString().hashCode();
}
@Override
public boolean equals( Object obj ) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
JcrPropertyDefinition other = (JcrPropertyDefinition)obj;
if (id == null) {
if (other.id != null) return false;
} else if (!id.equals(other.id)) return false;
return true;
}
/**
* Interface that encapsulates a reusable method that can test values to determine if they match a specific list of
* constraints for the semantics associated with a single {@link PropertyType}.
*/
public interface ConstraintChecker {
/**
* Returns the {@link PropertyType} (e.g., {@link PropertyType#LONG}) that defines the semantics used for interpretation
* for the constraint values.
*
* @return the {@link PropertyType} (e.g., {@link PropertyType#LONG}) that defines the semantics used for interpretation
* for the constraint values
*/
public abstract int getType();
/**
* Returns <code>true</code> if and only if <code>value</code> satisfies the constraints used to create this constraint
* checker.
*
* @param value the value to test
* @param session the session in which the constraints are to be checked; may not be nul
* @return whether or not the value satisfies the constraints used to create this constraint checker
* @see PropertyDefinition#getValueConstraints()
* @see JcrPropertyDefinition#satisfiesConstraints(Value,JcrSession)
*/
public abstract boolean matches( Value value,
JcrSession session );
public abstract boolean isAsOrMoreConstrainedThan( ConstraintChecker other );
}
private interface Range<T extends Comparable<T>> {
boolean accepts( T value );
T getMinimum();
T getMaximum();
boolean within( Range<T> other );
boolean includesLowerValue();
boolean includesUpperValue();
}
/**
* Encapsulation of common parsing logic used for all ranged constraints. Binary, long, double, and date values all have their
* constraints interpreted as a set of ranges that may include or exclude each end-point in the range.
*
* @param <T> the specific type of the constraint (e.g., Binary, Long, Double, or DateTime).
*/
private static abstract class RangeConstraintChecker<T extends Comparable<T>> implements ConstraintChecker {
private final Range<T>[] constraints;
private final ValueFactory<T> valueFactory;
private T minimumValue;
private T maximumValue;
@SuppressWarnings( "unchecked" )
protected RangeConstraintChecker( String[] valueConstraints,
ExecutionContext context ) {
constraints = new Range[valueConstraints.length];
this.valueFactory = getValueFactory(context.getValueFactories());
for (int i = 0; i < valueConstraints.length; i++) {
constraints[i] = parseValueConstraint(valueConstraints[i]);
}
}
protected abstract ValueFactory<T> getValueFactory( ValueFactories valueFactories );
@Override
public String toString() {
return constraints.toString();
}
@SuppressWarnings( "unchecked" )
protected T getMinimum() {
if (minimumValue == null) {
// This is idempotent, so okay to recreate ...
Comparable<T> minimum = null;
// Go through the value constraints and see which one is the minimum value ...
for (Range<T> range : constraints) {
T rangeMin = range.getMinimum();
if (rangeMin == null) continue;
if (minimum == null) {
minimum = rangeMin;
} else {
minimum = minimum.compareTo(rangeMin) > 0 ? rangeMin : minimum;
}
}
minimumValue = (T)minimum;
}
return minimumValue;
}
@SuppressWarnings( "unchecked" )
protected T getMaximum() {
if (maximumValue == null) {
// This is idempotent, so okay to recreate ...
Comparable<T> maximum = null;
// Go through the value constraints and see which one is the minimum value ...
for (Range<T> range : constraints) {
T rangeMax = range.getMaximum();
if (rangeMax == null) continue;
if (maximum == null) {
maximum = rangeMax;
} else {
maximum = maximum.compareTo(rangeMax) > 0 ? rangeMax : maximum;
}
}
maximumValue = (T)maximum;
}
return maximumValue;
}
/**
* Parses one constraint value into a {@link Range} that will accept only values which match the range described by the
* value constraint.
*
* @param valueConstraint the individual value constraint to be parsed into a {@link Range}.
* @return a range that accepts values which match the given value constraint.
*/
private Range<T> parseValueConstraint( final String valueConstraint ) {
assert valueConstraint != null;
final boolean includeLower = valueConstraint.charAt(0) == '[';
final boolean includeUpper = valueConstraint.charAt(valueConstraint.length() - 1) == ']';
int commaInd = valueConstraint.indexOf(',');
String lval = commaInd > 1 ? valueConstraint.substring(1, commaInd) : null;
String rval = commaInd < valueConstraint.length() - 2 ? valueConstraint.substring(commaInd + 1,
valueConstraint.length() - 1) : null;
final T lower = lval == null ? null : valueFactory.create(lval.trim());
final T upper = rval == null ? null : valueFactory.create(rval.trim());
return new Range<T>() {
@Override
public boolean accepts( T value ) {
if (lower != null && (includeLower ? lower.compareTo(value) > 0 : lower.compareTo(value) >= 0)) {
return false;
}
if (upper != null && (includeUpper ? upper.compareTo(value) < 0 : upper.compareTo(value) <= 0)) {
return false;
}
return true;
}
@Override
public String toString() {
return valueConstraint;
}
@Override
public T getMaximum() {
return upper;
}
@Override
public T getMinimum() {
return lower;
}
@Override
public boolean includesLowerValue() {
return includeLower;
}
@Override
public boolean includesUpperValue() {
return includeUpper;
}
@Override
public boolean within( Range<T> other ) {
T otherMin = other.getMinimum();
if (lower == null) {
if (otherMin != null) return false;
// Neither has a lower value (i.e., both null) so okay
} else if (otherMin != null) {
// Both have a non-null lower value ...
if (includeLower == other.includesLowerValue() || other.includesLowerValue()) {
if (lower.compareTo(otherMin) < 0) return false;
} else {
assert includeLower && !other.includesLowerValue();
if (lower.compareTo(otherMin) <= 0) return false;
}
}
T otherMax = other.getMaximum();
if (upper == null) {
if (otherMax != null) return false;
// Neither has an upper value (i.e., both null) so okay
} else if (otherMax != null) {
// Both have a non-null upper value ...
if (includeUpper == other.includesUpperValue() || other.includesUpperValue()) {
if (upper.compareTo(otherMax) > 0) return false;
} else {
assert includeUpper && !other.includesUpperValue();
if (upper.compareTo(otherMax) >= 0) return false;
}
}
return true;
}
};
}
@Override
public boolean matches( Value value,
JcrSession session ) {
assert value != null;
T convertedValue = valueFactory.create(((JcrValue)value).value());
for (int i = 0; i < constraints.length; i++) {
if (constraints[i].accepts(convertedValue)) {
return true;
}
}
return false;
}
@SuppressWarnings( "unchecked" )
@Override
public boolean isAsOrMoreConstrainedThan( ConstraintChecker other ) {
if (!other.getClass().equals(this.getClass())) return false;
RangeConstraintChecker<T> that = (RangeConstraintChecker<T>)other;
// Each of the ranges must be within one other range ...
for (Range<T> thisRange : this.constraints) {
boolean found = false;
for (Range<T> thatRange : that.constraints) {
if (thisRange.within(thatRange)) {
found = true;
break;
}
}
if (!found) return false;
}
return true;
}
}
@Immutable
private static class BinaryConstraintChecker extends LongConstraintChecker {
protected BinaryConstraintChecker( String[] valueConstraints,
ExecutionContext context ) {
super(valueConstraints, context);
}
@Override
public int getType() {
return PropertyType.BINARY;
}
@Override
public boolean matches( Value value,
JcrSession session ) {
try {
JcrValue jcrValue = (JcrValue)value;
long thatSize = value.getBinary().getSize();
JcrValue sizeValue = new JcrValue(jcrValue.factories(), PropertyType.LONG, thatSize);
return super.matches(sizeValue, session);
} catch (RepositoryException e) {
assert false : "Unexpected condition";
return false;
}
}
}
@Immutable
private static class LongConstraintChecker extends RangeConstraintChecker<Long> {
protected LongConstraintChecker( String[] valueConstraints,
ExecutionContext context ) {
super(valueConstraints, context);
}
@Override
public int getType() {
return PropertyType.LONG;
}
@Override
protected ValueFactory<Long> getValueFactory( ValueFactories valueFactories ) {
return valueFactories.getLongFactory();
}
}
@Immutable
private static class DateTimeConstraintChecker extends RangeConstraintChecker<DateTime> {
protected DateTimeConstraintChecker( String[] valueConstraints,
ExecutionContext context ) {
super(valueConstraints, context);
}
@Override
public int getType() {
return PropertyType.DATE;
}
@Override
protected ValueFactory<DateTime> getValueFactory( ValueFactories valueFactories ) {
return valueFactories.getDateFactory();
}
}
@Immutable
private static class DoubleConstraintChecker extends RangeConstraintChecker<Double> {
protected DoubleConstraintChecker( String[] valueConstraints,
ExecutionContext context ) {
super(valueConstraints, context);
}
@Override
public int getType() {
return PropertyType.DOUBLE;
}
@Override
protected ValueFactory<Double> getValueFactory( ValueFactories valueFactories ) {
return valueFactories.getDoubleFactory();
}
}
@Immutable
private static class DecimalConstraintChecker extends RangeConstraintChecker<BigDecimal> {
protected DecimalConstraintChecker( String[] valueConstraints,
ExecutionContext context ) {
super(valueConstraints, context);
}
@Override
public int getType() {
return PropertyType.DECIMAL;
}
@Override
protected ValueFactory<BigDecimal> getValueFactory( ValueFactories valueFactories ) {
return valueFactories.getDecimalFactory();
}
}
@Immutable
private static class ReferenceConstraintChecker implements ConstraintChecker {
private final Name[] constraints;
ExecutionContext context;
protected ReferenceConstraintChecker( String[] valueConstraints,
ExecutionContext context ) {
this.context = context;
NameFactory factory = context.getValueFactories().getNameFactory();
constraints = new Name[valueConstraints.length];
for (int i = 0; i < valueConstraints.length; i++) {
constraints[i] = factory.create(valueConstraints[i]);
}
}
@Override
public int getType() {
return PropertyType.REFERENCE;
}
@Override
public String toString() {
return asString(constraints, context);
}
@Override
public boolean matches( Value value,
JcrSession session ) {
assert value instanceof JcrValue;
if (session == null) {
return false;
}
JcrValue jcrValue = (JcrValue)value;
Node node = null;
try {
node = session.getNodeByIdentifier(jcrValue.getString());
} catch (RepositoryException re) {
return false;
}
NamespaceRegistry namespaces = session.namespaces();
for (int i = 0; i < constraints.length; i++) {
try {
if (node.isNodeType(constraints[i].getString(namespaces))) {
return true;
}
} catch (RepositoryException re) {
throw new IllegalStateException(re);
}
}
return false;
}
@Override
public boolean isAsOrMoreConstrainedThan( ConstraintChecker other ) {
if (!other.getClass().equals(this.getClass())) return false;
ReferenceConstraintChecker that = (ReferenceConstraintChecker)other;
// Compute the set of names from 'that' ...
Set<Name> thatNames = new HashSet<Name>();
for (Name name : that.constraints) {
thatNames.add(name);
}
// Every name in this must be found in that (but 'that' can have more) ...
for (Name name : this.constraints) {
if (!thatNames.contains(name)) return false;
}
return true;
}
}
private static class SimpleReferenceConstraintChecker extends ReferenceConstraintChecker {
protected SimpleReferenceConstraintChecker( String[] valueConstraints,
ExecutionContext context ) {
super(valueConstraints, context);
}
@Override
public int getType() {
return org.modeshape.jcr.api.PropertyType.SIMPLE_REFERENCE;
}
}
@Immutable
private static class NameConstraintChecker implements ConstraintChecker {
private final Name[] constraints;
private final ValueFactory<Name> valueFactory;
private final ExecutionContext context;
protected NameConstraintChecker( String[] valueConstraints,
ExecutionContext context ) {
this.context = context;
this.valueFactory = context.getValueFactories().getNameFactory();
constraints = new Name[valueConstraints.length];
for (int i = 0; i < valueConstraints.length; i++) {
constraints[i] = valueFactory.create(valueConstraints[i]);
}
}
@Override
public String toString() {
return asString(constraints, context);
}
@Override
public int getType() {
return PropertyType.NAME;
}
@Override
public boolean matches( Value value,
JcrSession session ) {
assert value instanceof JcrValue;
JcrValue jcrValue = (JcrValue)value;
// Need to use the session execution context to handle the remaps
Name name = valueFactory.create(jcrValue.value());
for (int i = 0; i < constraints.length; i++) {
if (constraints[i].equals(name)) {
return true;
}
}
return false;
}
@Override
public boolean isAsOrMoreConstrainedThan( ConstraintChecker other ) {
if (!other.getClass().equals(this.getClass())) return false;
NameConstraintChecker that = (NameConstraintChecker)other;
// Compute the set of names from 'that' ...
Set<Name> thatNames = new HashSet<Name>();
for (Name name : that.constraints) {
thatNames.add(name);
}
// Every name in this must be found in that (but 'that' can have more) ...
for (Name name : this.constraints) {
if (!thatNames.contains(name)) return false;
}
return true;
}
}
@Immutable
private static class StringConstraintChecker implements ConstraintChecker {
private final Set<String> expressions = new HashSet<String>();
private final Pattern[] constraints;
private ValueFactory<String> valueFactory;
protected StringConstraintChecker( String[] valueConstraints,
ExecutionContext context ) {
constraints = new Pattern[valueConstraints.length];
this.valueFactory = context.getValueFactories().getStringFactory();
for (int i = 0; i < valueConstraints.length; i++) {
String expr = valueConstraints[i];
try {
constraints[i] = Pattern.compile(expr);
} catch (Exception e) {
throw new ValueFormatException(expr, org.modeshape.jcr.value.PropertyType.STRING, "Invalid string pattern ");
}
expressions.add(expr);
}
}
@Override
public int getType() {
return PropertyType.STRING;
}
@Override
public boolean matches( Value value,
JcrSession session ) {
assert value != null;
String convertedValue = valueFactory.create(((JcrValue)value).value());
for (int i = 0; i < constraints.length; i++) {
if (constraints[i].matcher(convertedValue).matches()) {
return true;
}
}
return false;
}
@Override
public boolean isAsOrMoreConstrainedThan( ConstraintChecker other ) {
if (!other.getClass().equals(this.getClass())) return false;
StringConstraintChecker that = (StringConstraintChecker)other;
// Every regex in this must be found in that (but 'that' can have more) ...
for (String expression : this.expressions) {
if (!that.expressions.contains(expression)) return false;
}
return true;
}
}
@Immutable
private static class PathConstraintChecker implements ConstraintChecker {
private final ExecutionContext context;
private final String[] constraints;
protected PathConstraintChecker( String[] valueConstraints,
ExecutionContext context ) {
this.constraints = valueConstraints;
this.context = context;
}
@Override
public int getType() {
return PropertyType.PATH;
}
@Override
public String toString() {
return constraints.toString();
}
@Override
public boolean matches( Value valueToMatch,
JcrSession session ) {
assert valueToMatch instanceof JcrValue;
if (session == null) {
return false;
}
/*
* Need two path factories here. One uses the permanent namespace mappings to parse the constraints.
* The other also looks at the transient mappings to parse the checked value
*/
PathFactory repoPathFactory = context.getValueFactories().getPathFactory();
PathFactory sessionPathFactory = session.pathFactory();
Path value = sessionPathFactory.create(((JcrValue)valueToMatch).value());
value = value.getNormalizedPath();
for (int i = 0; i < constraints.length; i++) {
boolean matchesDescendants = constraints[i].endsWith("/*");
String pathStr = constraints[i];
if (matchesDescendants) pathStr = pathStr.substring(0, pathStr.length() - 2);
Path constraintPath = repoPathFactory.create(pathStr);
if (matchesDescendants && value.isDescendantOf(constraintPath)) {
return true;
}
if (!matchesDescendants && value.equals(constraintPath)) {
return true;
}
}
return false;
}
@Override
public boolean isAsOrMoreConstrainedThan( ConstraintChecker other ) {
if (!other.getClass().equals(this.getClass())) return false;
PathConstraintChecker that = (PathConstraintChecker)other;
// We only need the main path factory, since all paths are defined in node types ...
PathFactory pathFactory = context.getValueFactories().getPathFactory();
Set<Path> thatWildcardPaths = new HashSet<Path>();
Set<Path> thatExactPaths = new HashSet<Path>();
for (String constraint : that.constraints) {
boolean matchesDescendants = constraint.endsWith("/*");
if (matchesDescendants) {
String pathStr = constraint.substring(0, constraint.length() - 2);
Path path = pathFactory.create(pathStr);
thatWildcardPaths.add(path);
} else {
Path path = pathFactory.create(constraint);
thatExactPaths.add(path);
}
}
// Every path in this must be equal to or a descendant of a path in that ...
for (String constraint : this.constraints) {
Path path = pathFactory.create(constraint);
boolean matched = false;
// Check the exact match paths first ...
if (thatExactPaths.contains(path)) {
matched = true;
}
if (!matched) {
// Now check the wildcard paths ...
for (Path thatPath : thatWildcardPaths) {
if (path.isAtOrBelow(thatPath)) {
matched = true;
break;
}
}
if (!matched) return false;
}
}
return true;
}
}
private static class BooleanConstraintChecker implements ConstraintChecker {
private final Boolean constraint;
private final ValueFactories valueFactories;
protected BooleanConstraintChecker( ExecutionContext executionContext,
String... constraints ) {
this.valueFactories = executionContext.getValueFactories();
if (constraints != null && constraints.length > 0) {
constraint = valueFactories.getBooleanFactory().create(constraints[0]);
} else {
constraint = null;
}
}
@Override
public int getType() {
return PropertyType.BOOLEAN;
}
@Override
public boolean matches( Value value,
JcrSession session ) {
try {
return constraint == null || (value.getBoolean() && constraint);
} catch (RepositoryException e) {
return false;
}
}
@Override
public boolean isAsOrMoreConstrainedThan( ConstraintChecker other ) {
if (!other.getClass().equals(this.getClass())) {
return false;
}
Boolean otherConstraint = ((BooleanConstraintChecker)other).getConstraint();
return otherConstraint == null || otherConstraint.equals(constraint);
}
private Boolean getConstraint() {
return constraint;
}
}
protected static String asString( Object[] values,
ExecutionContext context ) {
if (values.length == 0) return "[]";
StringFactory strings = context.getValueFactories().getStringFactory();
StringBuilder sb = new StringBuilder();
sb.append('[');
sb.append(strings.create(values[0]));
for (int i = 1; i != values.length; ++i) {
sb.append(',');
sb.append(strings.create(values[0]));
}
return sb.toString();
}
}