package eu.fbk.knowledgestore.data;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import org.openrdf.model.URI;
/**
* Merge criteria for combining old and new values of record properties.
* <p>
* A {@code Criteria} represents (a set of) merge criteria for combining old and new values of
* selected record properties. Merge criteria are specified when updating records in the
* KnowledgeStore via its API, and can be also used on their own for manipulating records on the
* client side.
* </p>
* <p>
* The set of properties a {@code Criteria} object supports may be restricted. Method
* {@link #getProperties()} returns this set; the result is empty if the {@code Criteria} object
* supports any property. Methods {@link #appliesTo(URI)} and {@link #appliesToAll()} can be
* conveniently used for testing whether a specific property or all possible properties are
* respectively supported by a given {@code Criteria} object.
* </p>
* <p>
* Merging can be performed at two levels:
* </p>
* <ul>
* <li>on a single property, via method {@link #merge(URI, List, List)} that returns the list
* produced by merging old and new values, without modifying its inputs;</li>
* <li>over multiple properties in common to a pair of records, via method
* {@link #merge(Record, Record)}; this method merges values of any property in common to the old
* and new record which is supported by the {@code Criteria}, storing the resulting values in the
* old record, which is thus modified in place; if modification of input parameters is not
* desired, the caller may clone the old record in advance and supply the clone to the
* {@code merge()} method.</li>
* </ul>
* <p>
* The following {@code Criteria} are supported, and can be instantiated based on specific factory
* methods:
* </p>
* <ul>
* <li><i>overwrite criteria</i> (factory method {@link #overwrite(URI...)}), consisting in the
* discarding of old values which are overwritten with new values (even if new values are the
* empty list, which means the affected property is cleared);</li>
* <li><i>update criteria</i> (factory method {@link #update(URI...)}), consisting in the
* replacement of old values with new ones, but only if the list of new values is not empty
* (otherwise, old values are kept);</li>
* <li><i>init criteria</i> (factory method {@link #init(URI...)}), consisting in the assignment
* of new values only if the list of old values is empty, i.e., the property is initialized to the
* supplied of new values if previously unset, otherwise old values are kept;</li>
* <li><i>union criteria</i> (factory method {@link #union(URI...)}), consisting in computing and
* assigning the union of old and new values, removing duplicates (in which case the new value is
* kept, which for nested records means that properties of old nested records are discarded);</li>
* <li><i>min criteria</i> (factory method {@link #min(URI...)}), consisting in identifying and
* assigning the minimum value among the old and new ones (the comparator returned by
* {@link Data#getTotalComparator()} is used);</li>
* <li><i>max criteria</i> (factory method {@link #max(URI...)}), consisting in identifying and
* assigning the maximum value among the old and new ones (the comparator returned by
* {@link Data#getTotalComparator()} is used);</li>
* <li><i>composed criteria</i> (factory method {@link #compose(Criteria...)}), consisting in
* applying the first matching {@code Criteria} of the specified list to an input property; the
* resulting {@code Criteria} will be able to merge the union of all the properties supported by
* the composed {@code Criteria}. In case decomposition of a possibly composed {@code Criteria} is
* desired, method {@link #decompose()} returns the (recursively) composed elementary
* {@code Criteria} for a certain input {@code Criteria} (the input {@code Criteria} is returned
* unchanged if not composes).</li>
* </ul>
* <p>
* {@code Criteria} objects are immutable and thus thread safe. Two {@code Criteria} objects are
* equal if they implement the same strategy (possibly composed) and support the same properties.
* Serialization to string and deserialization back to a {@code Criteria} object are supported,
* via methods {@link #toString()}, {@link #toString(Map)} (which accepts a custom namespace map
* for encoding properties) and {@link #parse(String, Map)}. The string specification of a
* {@code Criteria} has the following form:
* {@code criteria1 property11, ..., property1N, ..., criteriaM propertyM1, ... propertyMN} where
* commas are all optional, the criteria token is one of {@code overwrite}, {@code update},
* {@code init}, {@code union}, {@code min}, {@code max} (case does not matter) and properties are
* encoded according to the Turtle / TriG syntax (full URIs between {@code <} and {@code >}
* characters or QNames).
* </p>
*/
public abstract class Criteria implements Serializable {
private static final long serialVersionUID = 1L;
private final Set<URI> properties;
private Criteria(final URI... properties) {
this.properties = ImmutableSet.copyOf(properties);
}
private static Criteria create(final String name, final URI... properties) {
if (Overwrite.class.getSimpleName().equalsIgnoreCase(name)) {
return overwrite(properties);
} else if (Update.class.getSimpleName().equalsIgnoreCase(name)) {
return update(properties);
} else if (Init.class.getSimpleName().equalsIgnoreCase(name)) {
return init(properties);
} else if (Min.class.getSimpleName().equalsIgnoreCase(name)) {
return min(properties);
} else if (Max.class.getSimpleName().equalsIgnoreCase(name)) {
return max(properties);
} else if (Union.class.getSimpleName().equalsIgnoreCase(name)) {
return union(properties);
} else {
throw new IllegalArgumentException("Unknown criteria name: " + name);
}
}
/**
* Parses the supplied string specification of a merge criteria, returning the parsed
* {@code Criteria} object. The string must adhere to the format specified in the main Javadoc
* comment.
*
* @param string
* the specification of the merge criteria
* @param namespaces
* the namespace map to be used for parsing the string, null if no mapping should
* be used
* @return the parsed {@code Criteria}, on success
* @throws ParseException
* in case the specification string is not valid
*/
public static Criteria parse(final String string,
@Nullable final Map<String, String> namespaces) throws ParseException {
Preconditions.checkNotNull(string);
final List<Criteria> criteria = Lists.newArrayList();
final List<URI> uris = Lists.newArrayList();
String name = null;
try {
for (final String token : string.split("[\\s\\,]+")) {
if ("*".equals(token)) {
criteria.add(create(name, uris.toArray(new URI[uris.size()])));
name = null;
uris.clear();
} else if (token.startsWith("<") && token.endsWith(">") //
|| token.indexOf(':') >= 0) {
uris.add((URI) Data.parseValue(token, namespaces));
} else if (name != null || !uris.isEmpty()) {
criteria.add(create(name, uris.toArray(new URI[uris.size()])));
name = token;
uris.clear();
} else {
name = token;
}
}
if (!uris.isEmpty()) {
criteria.add(create(name, uris.toArray(new URI[uris.size()])));
}
return criteria.size() == 1 ? criteria.get(0) : compose(criteria
.toArray(new Criteria[criteria.size()]));
} catch (final Exception ex) {
throw new ParseException(string, "Invalid criteria string - " + ex.getMessage(), ex);
}
}
/**
* Creates a {@code Criteria} object implementing the <i>overwrite</i> merge criteria for the
* properties specified. The overwrite criteria always selects the new values of a property,
* even if they consist in an empty list that will cause the property to be cleared.
*
* @param properties
* a vararg array with the properties over which the criteria should be applied; if
* empty, the criteria will be applied to any property
* @return the created {@code Criteria} object
*/
public static Criteria overwrite(final URI... properties) {
return new Overwrite(properties);
}
/**
* Creates a {@code Criteria} object implementing the <i>update</i> merge criteria for the
* properties specified. The update criteria assigns the new values to a property only if they
* do not consist in the empty list, in which case old values are kept.
*
* @param properties
* a vararg array with the properties over which the criteria should be applied; if
* empty, the criteria will be applied to any property
* @return the created {@code Criteria} object
*/
public static Criteria update(final URI... properties) {
return new Update(properties);
}
/**
* Creates a {@code Criteria} object implementing the <i>init</i> merge criteria for the
* properties specified. The init criteria assignes the new values to a property only if it
* has no old value (i.e., old values are the empty list), thus realizing a one-time property
* initialization mechanism.
*
* @param properties
* a vararg array with the properties over which the criteria should be applied; if
* empty, the criteria will be applied to any property
* @return the created {@code Criteria} object
*/
public static Criteria init(final URI... properties) {
return new Init(properties);
}
/**
* Creates a {@code Criteria} object implementing the <i>union</i> merge criteria for the
* properties specified. The union criteria assigns the union of old and new values to a
* property, discarding duplicates. In case duplicates are two nested records with the same ID
* (thus evaluating equal), the new value is kept.
*
* @param properties
* a vararg array with the properties over which the criteria should be applied; if
* empty, the criteria will be applied to any property
* @return the created {@code Criteria} object
*/
public static Criteria union(final URI... properties) {
return new Union(properties);
}
/**
* Creates a {@code Criteria} object implementing the <i>min</i> merge criteria for the
* properties specified. The min criteria assignes the minimum value among the old and new
* ones.
*
* @param properties
* a vararg array with the properties over which the criteria should be applied; if
* empty, the criteria will be applied to any property
* @return the created {@code Criteria} object
*/
public static Criteria min(final URI... properties) {
return new Min(properties);
}
/**
* Creates a {@code Criteria} object implementing the <i>max</i> merge criteria for the
* properties specified. The max criteria assignes the maximum value among the old and new
* ones.
*
* @param properties
* a vararg array with the properties over which the criteria should be applied; if
* empty, the criteria will be applied to any property
* @return the created {@code Criteria} object
*/
public static Criteria max(final URI... properties) {
return new Max(properties);
}
/**
* Creates a {@code Criteria} object that composes the {@code Criteria} objects specified in a
* <i>composed</i> merge criteria. Given a property whose old and new values have to be
* merged, the created composed criteria will scan through the supplied list of
* {@code Criteria} using the first matching one. As a consequence, the created criteria will
* support all the properties that are supported by at least one of the composed
* {@code Criteria}; if one of them supports all the properties, then the composed criteria
* will also support all the properties.
*
* @param criteria
* the {@code Criteria} objects to compose
* @return the created {@code Criteria} object
*/
public static Criteria compose(final Criteria... criteria) {
Preconditions.checkArgument(criteria.length > 0, "At least a criteria must be supplied");
if (criteria.length == 1) {
return criteria[0];
} else {
return new Compose(criteria);
}
}
/**
* Returns the set of properties supported by this {@code Criteria} object.
*
* @return a set with the supported properties; if empty, all properties are supported
*/
public final Set<URI> getProperties() {
return this.properties;
}
/**
* Checks whether the property specified is supported by this {@code Criteria} object.
*
* @param property
* the property
* @return true, if the property is supported
*/
public final boolean appliesTo(final URI property) {
if (this.properties.isEmpty() || this.properties.contains(property)) {
return true;
}
Preconditions.checkNotNull(property);
return false;
}
/**
* Checks whether all properties are supported by this {@code Criteria} object.
*
* @return true, if all properties are supported, with no restriction
*/
public final boolean appliesToAll() {
return this.properties.isEmpty();
}
/**
* Merges all supported properties in common to the old and new record specified, storing the
* results in the old record.
*
* @param oldRecord
* the record containing old property values, not null; results of the merging
* operation are stored in this record, possibly replacing old values of affected
* properties (if this behavious is not desired, clone the old record in advance)
* @param newRecord
* the record containing new property values, not null
*/
public final void merge(final Record oldRecord, final Record newRecord) {
Preconditions.checkNotNull(oldRecord);
for (final URI property : newRecord.getProperties()) {
if (appliesTo(property)) {
oldRecord.set(property,
merge(property, oldRecord.get(property), newRecord.get(property)));
}
}
}
/**
* Merges old and new values of the property specified, returning the resulting list of
* values. Input value lists are not affected. In case the property specified is not supported
* by this {@code Criteria} object, the old list of values is returned.
*
* @param property
* the property to merge (used to control whether merging should be performed and
* which strategy should be adopted in case of a composed {@code Criteria} object)
* @param oldValues
* a list with the old values of the property, not null
* @param newValues
* a list with the new values of the property, not null
* @return the list of values obtained by applying the merge criteria
*/
@SuppressWarnings("unchecked")
public final List<Object> merge(final URI property, final List<? extends Object> oldValues,
final List<? extends Object> newValues) {
Preconditions.checkNotNull(oldValues);
Preconditions.checkNotNull(newValues);
return doMerge(property, (List<Object>) oldValues, (List<Object>) newValues);
}
List<Object> doMerge(final URI property, final List<Object> oldValues,
final List<Object> newValues) {
return appliesTo(property) ? doMerge(oldValues, newValues) : oldValues;
}
List<Object> doMerge(final List<Object> oldValues, final List<Object> newValues) {
return oldValues;
}
/**
* Decomposes this {@code Criteria} object in its elementary (i.e., non-composed) components.
* In case this {@code Criteria} object is not composed, it is directly returned by the method
* in a singleton list. Otherwise, its components are recursively extracted and returned in a
* list that reflects their order of use.
*
* @return a list of non-composed {@code Criteria} objects, in the same order as they are
* applied in this merge criteria object
*/
public final List<Criteria> decompose() {
return doDecompose();
}
List<Criteria> doDecompose() {
return ImmutableList.of(this);
}
/**
* {@inheritDoc} Two {@code Criteria} objects are equal if they implement the same merge
* criteria over the same properties.
*/
@Override
public final boolean equals(final Object object) {
if (object == this) {
return true;
}
if (object == null || object.getClass() != this.getClass()) {
return false;
}
final Criteria other = (Criteria) object;
return this.properties.equals(other.getProperties());
}
/**
* {@inheritDoc} The returned hash code reflects the specific criteria and supported
* properties of this {@code Criteria} object.
*/
@Override
public final int hashCode() {
return this.properties.hashCode();
}
/**
* Returns a parseable string representation of this {@code Criteria} object, using the
* supplied namespace map for encoding property URIs.
*
* @param namespaces
* the namespace map to encode property URIs
* @return the produced string
*/
public final String toString(@Nullable final Map<String, String> namespaces) {
final StringBuilder builder = new StringBuilder();
doToString(builder, namespaces);
return builder.toString();
}
/**
* {@inheritDoc} This method returns a parseable string representation of this
* {@code Criteria} object, encoding property URIs as full, non-abbreviated URIs.
*/
@Override
public final String toString() {
return toString(null);
}
void doToString(final StringBuilder builder, @Nullable final Map<String, String> namespaces) {
builder.append(getClass().getSimpleName().toLowerCase()).append(" ");
if (this.properties.isEmpty()) {
builder.append("*");
} else {
String separator = "";
for (final URI property : this.properties) {
builder.append(separator).append(Data.toString(property, namespaces));
separator = ", ";
}
}
}
private static final class Overwrite extends Criteria {
private static final long serialVersionUID = 1L;
Overwrite(final URI... properties) {
super(properties);
}
@Override
List<Object> doMerge(final List<Object> oldValues, final List<Object> newValues) {
return newValues;
}
}
private static final class Update extends Criteria {
private static final long serialVersionUID = 1L;
Update(final URI... properties) {
super(properties);
}
@Override
List<Object> doMerge(final List<Object> oldValues, final List<Object> newValues) {
return newValues.isEmpty() ? oldValues : newValues;
}
}
private static final class Init extends Criteria {
private static final long serialVersionUID = 1L;
Init(final URI... properties) {
super(properties);
}
@Override
List<Object> doMerge(final List<Object> oldValues, final List<Object> newValues) {
return oldValues.isEmpty() ? newValues : oldValues;
}
}
private static final class Union extends Criteria {
private static final long serialVersionUID = 1L;
Union(final URI... properties) {
super(properties);
}
@Override
List<Object> doMerge(final List<Object> oldValues, final List<Object> newValues) {
if (oldValues.isEmpty()) {
return newValues;
} else if (newValues.isEmpty()) {
return oldValues;
} else {
final Set<Object> set = Sets.newLinkedHashSet();
set.addAll(oldValues);
set.addAll(newValues);
return ImmutableList.copyOf(set);
}
}
}
private static final class Min extends Criteria {
private static final long serialVersionUID = 1L;
Min(final URI... properties) {
super(properties);
}
@Override
List<Object> doMerge(final List<Object> oldValues, final List<Object> newValues) {
if (oldValues.isEmpty()) {
return newValues.size() <= 1 ? newValues : ImmutableList
.of(((Ordering<Object>) Data.getTotalComparator()).min(newValues));
} else if (newValues.isEmpty()) {
return oldValues.size() <= 1 ? oldValues : ImmutableList
.of(((Ordering<Object>) Data.getTotalComparator()).min(oldValues));
} else {
return ImmutableList.of(((Ordering<Object>) Data.getTotalComparator())
.min(Iterables.concat(oldValues, newValues)));
}
}
}
private static final class Max extends Criteria {
private static final long serialVersionUID = 1L;
Max(final URI... properties) {
super(properties);
}
@Override
List<Object> doMerge(final List<Object> oldValues, final List<Object> newValues) {
if (oldValues.isEmpty()) {
return newValues.size() <= 1 ? newValues : ImmutableList
.of(((Ordering<Object>) Data.getTotalComparator()).max(newValues));
} else if (newValues.isEmpty()) {
return oldValues.size() <= 1 ? oldValues : ImmutableList
.of(((Ordering<Object>) Data.getTotalComparator()).max(oldValues));
} else {
return ImmutableList.of(((Ordering<Object>) Data.getTotalComparator())
.max(Iterables.concat(oldValues, newValues)));
}
}
}
private static final class Compose extends Criteria {
private static final long serialVersionUID = 1L;
private final Criteria[] specificCriteria;
private final Criteria defaultCriteria;
Compose(final Criteria... criteria) {
super(extractProperties(criteria));
Criteria candidateDefaultCriteria = null;
final ImmutableList.Builder<Criteria> builder = ImmutableList.builder();
for (final Criteria c : criteria) {
for (final Criteria d : c.decompose()) {
if (!d.appliesToAll()) {
builder.add(d);
} else if (candidateDefaultCriteria == null) {
candidateDefaultCriteria = d;
}
}
}
this.specificCriteria = Iterables.toArray(builder.build(), Criteria.class);
this.defaultCriteria = candidateDefaultCriteria;
}
@Override
List<Object> doMerge(final URI property, final List<Object> oldValues,
final List<Object> newValues) {
for (final Criteria c : this.specificCriteria) {
if (c.appliesTo(property)) {
return c.doMerge(oldValues, newValues);
}
}
if (this.defaultCriteria != null) {
return this.defaultCriteria.doMerge(oldValues, newValues);
}
return oldValues;
}
@Override
List<Criteria> doDecompose() {
final ImmutableList.Builder<Criteria> builder = ImmutableList.builder();
builder.add(this.specificCriteria);
builder.add(this.defaultCriteria);
return builder.build();
}
@Override
void doToString(final StringBuilder builder, //
@Nullable final Map<String, String> namespaces) {
String separator = "";
for (final Criteria c : this.specificCriteria) {
builder.append(separator);
c.doToString(builder, namespaces);
separator = ", ";
}
if (this.defaultCriteria != null) {
builder.append(separator);
this.defaultCriteria.doToString(builder, namespaces);
}
}
private static URI[] extractProperties(final Criteria... criteria) {
final List<URI> properties = Lists.newArrayList();
for (final Criteria c : criteria) {
properties.addAll(c.properties);
}
return properties.toArray(new URI[properties.size()]);
}
}
}
// alternative API can be:
// (1) new Criteria().update(DC.TITLE).override(DC.ISSUED)
// - modifiable, verbose if a single option is used
// (2) Criteria.builder().update(DC.TITLE).override(DC.ISSUED).build()
// - verbose, esp if a single option is used
// alternative (2) can be however merged into this implementation, by adding a builder