1   package eu.fbk.knowledgestore.data;
2   
3   import java.io.Serializable;
4   import java.util.List;
5   import java.util.Map;
6   import java.util.Set;
7   
8   import javax.annotation.Nullable;
9   
10  import com.google.common.base.Preconditions;
11  import com.google.common.collect.ImmutableList;
12  import com.google.common.collect.ImmutableSet;
13  import com.google.common.collect.Iterables;
14  import com.google.common.collect.Lists;
15  import com.google.common.collect.Ordering;
16  import com.google.common.collect.Sets;
17  
18  import org.openrdf.model.URI;
19  
20  /**
21   * Merge criteria for combining old and new values of record properties.
22   * <p>
23   * A {@code Criteria} represents (a set of) merge criteria for combining old and new values of
24   * selected record properties. Merge criteria are specified when updating records in the
25   * KnowledgeStore via its API, and can be also used on their own for manipulating records on the
26   * client side.
27   * </p>
28   * <p>
29   * The set of properties a {@code Criteria} object supports may be restricted. Method
30   * {@link #getProperties()} returns this set; the result is empty if the {@code Criteria} object
31   * supports any property. Methods {@link #appliesTo(URI)} and {@link #appliesToAll()} can be
32   * conveniently used for testing whether a specific property or all possible properties are
33   * respectively supported by a given {@code Criteria} object.
34   * </p>
35   * <p>
36   * Merging can be performed at two levels:
37   * </p>
38   * <ul>
39   * <li>on a single property, via method {@link #merge(URI, List, List)} that returns the list
40   * produced by merging old and new values, without modifying its inputs;</li>
41   * <li>over multiple properties in common to a pair of records, via method
42   * {@link #merge(Record, Record)}; this method merges values of any property in common to the old
43   * and new record which is supported by the {@code Criteria}, storing the resulting values in the
44   * old record, which is thus modified in place; if modification of input parameters is not
45   * desired, the caller may clone the old record in advance and supply the clone to the
46   * {@code merge()} method.</li>
47   * </ul>
48   * <p>
49   * The following {@code Criteria} are supported, and can be instantiated based on specific factory
50   * methods:
51   * </p>
52   * <ul>
53   * <li><i>overwrite criteria</i> (factory method {@link #overwrite(URI...)}), consisting in the
54   * discarding of old values which are overwritten with new values (even if new values are the
55   * empty list, which means the affected property is cleared);</li>
56   * <li><i>update criteria</i> (factory method {@link #update(URI...)}), consisting in the
57   * replacement of old values with new ones, but only if the list of new values is not empty
58   * (otherwise, old values are kept);</li>
59   * <li><i>init criteria</i> (factory method {@link #init(URI...)}), consisting in the assignment
60   * of new values only if the list of old values is empty, i.e., the property is initialized to the
61   * supplied of new values if previously unset, otherwise old values are kept;</li>
62   * <li><i>union criteria</i> (factory method {@link #union(URI...)}), consisting in computing and
63   * assigning the union of old and new values, removing duplicates (in which case the new value is
64   * kept, which for nested records means that properties of old nested records are discarded);</li>
65   * <li><i>min criteria</i> (factory method {@link #min(URI...)}), consisting in identifying and
66   * assigning the minimum value among the old and new ones (the comparator returned by
67   * {@link Data#getTotalComparator()} is used);</li>
68   * <li><i>max criteria</i> (factory method {@link #max(URI...)}), consisting in identifying and
69   * assigning the maximum value among the old and new ones (the comparator returned by
70   * {@link Data#getTotalComparator()} is used);</li>
71   * <li><i>composed criteria</i> (factory method {@link #compose(Criteria...)}), consisting in
72   * applying the first matching {@code Criteria} of the specified list to an input property; the
73   * resulting {@code Criteria} will be able to merge the union of all the properties supported by
74   * the composed {@code Criteria}. In case decomposition of a possibly composed {@code Criteria} is
75   * desired, method {@link #decompose()} returns the (recursively) composed elementary
76   * {@code Criteria} for a certain input {@code Criteria} (the input {@code Criteria} is returned
77   * unchanged if not composes).</li>
78   * </ul>
79   * <p>
80   * {@code Criteria} objects are immutable and thus thread safe. Two {@code Criteria} objects are
81   * equal if they implement the same strategy (possibly composed) and support the same properties.
82   * Serialization to string and deserialization back to a {@code Criteria} object are supported,
83   * via methods {@link #toString()}, {@link #toString(Map)} (which accepts a custom namespace map
84   * for encoding properties) and {@link #parse(String, Map)}. The string specification of a
85   * {@code Criteria} has the following form:
86   * {@code criteria1 property11, ..., property1N, ..., criteriaM propertyM1, ... propertyMN} where
87   * commas are all optional, the criteria token is one of {@code overwrite}, {@code update},
88   * {@code init}, {@code union}, {@code min}, {@code max} (case does not matter) and properties are
89   * encoded according to the Turtle / TriG syntax (full URIs between {@code <} and {@code >}
90   * characters or QNames).
91   * </p>
92   */
93  public abstract class Criteria implements Serializable {
94  
95      private static final long serialVersionUID = 1L;
96  
97      private final Set<URI> properties;
98  
99      private Criteria(final URI... properties) {
100         this.properties = ImmutableSet.copyOf(properties);
101     }
102 
103     private static Criteria create(final String name, final URI... properties) {
104 
105         if (Overwrite.class.getSimpleName().equalsIgnoreCase(name)) {
106             return overwrite(properties);
107         } else if (Update.class.getSimpleName().equalsIgnoreCase(name)) {
108             return update(properties);
109         } else if (Init.class.getSimpleName().equalsIgnoreCase(name)) {
110             return init(properties);
111         } else if (Min.class.getSimpleName().equalsIgnoreCase(name)) {
112             return min(properties);
113         } else if (Max.class.getSimpleName().equalsIgnoreCase(name)) {
114             return max(properties);
115         } else if (Union.class.getSimpleName().equalsIgnoreCase(name)) {
116             return union(properties);
117         } else {
118             throw new IllegalArgumentException("Unknown criteria name: " + name);
119         }
120     }
121 
122     /**
123      * Parses the supplied string specification of a merge criteria, returning the parsed
124      * {@code Criteria} object. The string must adhere to the format specified in the main Javadoc
125      * comment.
126      * 
127      * @param string
128      *            the specification of the merge criteria
129      * @param namespaces
130      *            the namespace map to be used for parsing the string, null if no mapping should
131      *            be used
132      * @return the parsed {@code Criteria}, on success
133      * @throws ParseException
134      *             in case the specification string is not valid
135      */
136     public static Criteria parse(final String string,
137             @Nullable final Map<String, String> namespaces) throws ParseException {
138 
139         Preconditions.checkNotNull(string);
140 
141         final List<Criteria> criteria = Lists.newArrayList();
142         final List<URI> uris = Lists.newArrayList();
143         String name = null;
144 
145         try {
146             for (final String token : string.split("[\\s\\,]+")) {
147                 if ("*".equals(token)) {
148                     criteria.add(create(name, uris.toArray(new URI[uris.size()])));
149                     name = null;
150                     uris.clear();
151 
152                 } else if (token.startsWith("<") && token.endsWith(">") //
153                         || token.indexOf(':') >= 0) {
154                     uris.add((URI) Data.parseValue(token, namespaces));
155 
156                 } else if (name != null || !uris.isEmpty()) {
157                     criteria.add(create(name, uris.toArray(new URI[uris.size()])));
158                     name = token;
159                     uris.clear();
160 
161                 } else {
162                     name = token;
163                 }
164             }
165 
166             if (!uris.isEmpty()) {
167                 criteria.add(create(name, uris.toArray(new URI[uris.size()])));
168             }
169 
170             return criteria.size() == 1 ? criteria.get(0) : compose(criteria
171                     .toArray(new Criteria[criteria.size()]));
172 
173         } catch (final Exception ex) {
174             throw new ParseException(string, "Invalid criteria string - " + ex.getMessage(), ex);
175         }
176     }
177 
178     /**
179      * Creates a {@code Criteria} object implementing the <i>overwrite</i> merge criteria for the
180      * properties specified. The overwrite criteria always selects the new values of a property,
181      * even if they consist in an empty list that will cause the property to be cleared.
182      * 
183      * @param properties
184      *            a vararg array with the properties over which the criteria should be applied; if
185      *            empty, the criteria will be applied to any property
186      * @return the created {@code Criteria} object
187      */
188     public static Criteria overwrite(final URI... properties) {
189         return new Overwrite(properties);
190     }
191 
192     /**
193      * Creates a {@code Criteria} object implementing the <i>update</i> merge criteria for the
194      * properties specified. The update criteria assigns the new values to a property only if they
195      * do not consist in the empty list, in which case old values are kept.
196      * 
197      * @param properties
198      *            a vararg array with the properties over which the criteria should be applied; if
199      *            empty, the criteria will be applied to any property
200      * @return the created {@code Criteria} object
201      */
202     public static Criteria update(final URI... properties) {
203         return new Update(properties);
204     }
205 
206     /**
207      * Creates a {@code Criteria} object implementing the <i>init</i> merge criteria for the
208      * properties specified. The init criteria assignes the new values to a property only if it
209      * has no old value (i.e., old values are the empty list), thus realizing a one-time property
210      * initialization mechanism.
211      * 
212      * @param properties
213      *            a vararg array with the properties over which the criteria should be applied; if
214      *            empty, the criteria will be applied to any property
215      * @return the created {@code Criteria} object
216      */
217     public static Criteria init(final URI... properties) {
218         return new Init(properties);
219     }
220 
221     /**
222      * Creates a {@code Criteria} object implementing the <i>union</i> merge criteria for the
223      * properties specified. The union criteria assigns the union of old and new values to a
224      * property, discarding duplicates. In case duplicates are two nested records with the same ID
225      * (thus evaluating equal), the new value is kept.
226      * 
227      * @param properties
228      *            a vararg array with the properties over which the criteria should be applied; if
229      *            empty, the criteria will be applied to any property
230      * @return the created {@code Criteria} object
231      */
232     public static Criteria union(final URI... properties) {
233         return new Union(properties);
234     }
235 
236     /**
237      * Creates a {@code Criteria} object implementing the <i>min</i> merge criteria for the
238      * properties specified. The min criteria assignes the minimum value among the old and new
239      * ones.
240      * 
241      * @param properties
242      *            a vararg array with the properties over which the criteria should be applied; if
243      *            empty, the criteria will be applied to any property
244      * @return the created {@code Criteria} object
245      */
246     public static Criteria min(final URI... properties) {
247         return new Min(properties);
248     }
249 
250     /**
251      * Creates a {@code Criteria} object implementing the <i>max</i> merge criteria for the
252      * properties specified. The max criteria assignes the maximum value among the old and new
253      * ones.
254      * 
255      * @param properties
256      *            a vararg array with the properties over which the criteria should be applied; if
257      *            empty, the criteria will be applied to any property
258      * @return the created {@code Criteria} object
259      */
260     public static Criteria max(final URI... properties) {
261         return new Max(properties);
262     }
263 
264     /**
265      * Creates a {@code Criteria} object that composes the {@code Criteria} objects specified in a
266      * <i>composed</i> merge criteria. Given a property whose old and new values have to be
267      * merged, the created composed criteria will scan through the supplied list of
268      * {@code Criteria} using the first matching one. As a consequence, the created criteria will
269      * support all the properties that are supported by at least one of the composed
270      * {@code Criteria}; if one of them supports all the properties, then the composed criteria
271      * will also support all the properties.
272      * 
273      * @param criteria
274      *            the {@code Criteria} objects to compose
275      * @return the created {@code Criteria} object
276      */
277     public static Criteria compose(final Criteria... criteria) {
278         Preconditions.checkArgument(criteria.length > 0, "At least a criteria must be supplied");
279         if (criteria.length == 1) {
280             return criteria[0];
281         } else {
282             return new Compose(criteria);
283         }
284     }
285 
286     /**
287      * Returns the set of properties supported by this {@code Criteria} object.
288      * 
289      * @return a set with the supported properties; if empty, all properties are supported
290      */
291     public final Set<URI> getProperties() {
292         return this.properties;
293     }
294 
295     /**
296      * Checks whether the property specified is supported by this {@code Criteria} object.
297      * 
298      * @param property
299      *            the property
300      * @return true, if the property is supported
301      */
302     public final boolean appliesTo(final URI property) {
303 
304         if (this.properties.isEmpty() || this.properties.contains(property)) {
305             return true;
306         }
307         Preconditions.checkNotNull(property);
308         return false;
309     }
310 
311     /**
312      * Checks whether all properties are supported by this {@code Criteria} object.
313      * 
314      * @return true, if all properties are supported, with no restriction
315      */
316     public final boolean appliesToAll() {
317         return this.properties.isEmpty();
318     }
319 
320     /**
321      * Merges all supported properties in common to the old and new record specified, storing the
322      * results in the old record.
323      * 
324      * @param oldRecord
325      *            the record containing old property values, not null; results of the merging
326      *            operation are stored in this record, possibly replacing old values of affected
327      *            properties (if this behavious is not desired, clone the old record in advance)
328      * @param newRecord
329      *            the record containing new property values, not null
330      */
331     public final void merge(final Record oldRecord, final Record newRecord) {
332 
333         Preconditions.checkNotNull(oldRecord);
334 
335         for (final URI property : newRecord.getProperties()) {
336             if (appliesTo(property)) {
337                 oldRecord.set(property,
338                         merge(property, oldRecord.get(property), newRecord.get(property)));
339             }
340         }
341     }
342 
343     /**
344      * Merges old and new values of the property specified, returning the resulting list of
345      * values. Input value lists are not affected. In case the property specified is not supported
346      * by this {@code Criteria} object, the old list of values is returned.
347      * 
348      * @param property
349      *            the property to merge (used to control whether merging should be performed and
350      *            which strategy should be adopted in case of a composed {@code Criteria} object)
351      * @param oldValues
352      *            a list with the old values of the property, not null
353      * @param newValues
354      *            a list with the new values of the property, not null
355      * @return the list of values obtained by applying the merge criteria
356      */
357     @SuppressWarnings("unchecked")
358     public final List<Object> merge(final URI property, final List<? extends Object> oldValues,
359             final List<? extends Object> newValues) {
360 
361         Preconditions.checkNotNull(oldValues);
362         Preconditions.checkNotNull(newValues);
363 
364         return doMerge(property, (List<Object>) oldValues, (List<Object>) newValues);
365     }
366 
367     List<Object> doMerge(final URI property, final List<Object> oldValues,
368             final List<Object> newValues) {
369         return appliesTo(property) ? doMerge(oldValues, newValues) : oldValues;
370     }
371 
372     List<Object> doMerge(final List<Object> oldValues, final List<Object> newValues) {
373         return oldValues;
374     }
375 
376     /**
377      * Decomposes this {@code Criteria} object in its elementary (i.e., non-composed) components.
378      * In case this {@code Criteria} object is not composed, it is directly returned by the method
379      * in a singleton list. Otherwise, its components are recursively extracted and returned in a
380      * list that reflects their order of use.
381      * 
382      * @return a list of non-composed {@code Criteria} objects, in the same order as they are
383      *         applied in this merge criteria object
384      */
385     public final List<Criteria> decompose() {
386         return doDecompose();
387     }
388 
389     List<Criteria> doDecompose() {
390         return ImmutableList.of(this);
391     }
392 
393     /**
394      * {@inheritDoc} Two {@code Criteria} objects are equal if they implement the same merge
395      * criteria over the same properties.
396      */
397     @Override
398     public final boolean equals(final Object object) {
399         if (object == this) {
400             return true;
401         }
402         if (object == null || object.getClass() != this.getClass()) {
403             return false;
404         }
405         final Criteria other = (Criteria) object;
406         return this.properties.equals(other.getProperties());
407     }
408 
409     /**
410      * {@inheritDoc} The returned hash code reflects the specific criteria and supported
411      * properties of this {@code Criteria} object.
412      */
413     @Override
414     public final int hashCode() {
415         return this.properties.hashCode();
416     }
417 
418     /**
419      * Returns a parseable string representation of this {@code Criteria} object, using the
420      * supplied namespace map for encoding property URIs.
421      * 
422      * @param namespaces
423      *            the namespace map to encode property URIs
424      * @return the produced string
425      */
426     public final String toString(@Nullable final Map<String, String> namespaces) {
427         final StringBuilder builder = new StringBuilder();
428         doToString(builder, namespaces);
429         return builder.toString();
430     }
431 
432     /**
433      * {@inheritDoc} This method returns a parseable string representation of this
434      * {@code Criteria} object, encoding property URIs as full, non-abbreviated URIs.
435      */
436     @Override
437     public final String toString() {
438         return toString(null);
439     }
440 
441     void doToString(final StringBuilder builder, @Nullable final Map<String, String> namespaces) {
442         builder.append(getClass().getSimpleName().toLowerCase()).append(" ");
443         if (this.properties.isEmpty()) {
444             builder.append("*");
445         } else {
446             String separator = "";
447             for (final URI property : this.properties) {
448                 builder.append(separator).append(Data.toString(property, namespaces));
449                 separator = ", ";
450             }
451         }
452     }
453 
454     private static final class Overwrite extends Criteria {
455 
456         private static final long serialVersionUID = 1L;
457 
458         Overwrite(final URI... properties) {
459             super(properties);
460         }
461 
462         @Override
463         List<Object> doMerge(final List<Object> oldValues, final List<Object> newValues) {
464             return newValues;
465         }
466 
467     }
468 
469     private static final class Update extends Criteria {
470 
471         private static final long serialVersionUID = 1L;
472 
473         Update(final URI... properties) {
474             super(properties);
475         }
476 
477         @Override
478         List<Object> doMerge(final List<Object> oldValues, final List<Object> newValues) {
479             return newValues.isEmpty() ? oldValues : newValues;
480         }
481 
482     }
483 
484     private static final class Init extends Criteria {
485 
486         private static final long serialVersionUID = 1L;
487 
488         Init(final URI... properties) {
489             super(properties);
490         }
491 
492         @Override
493         List<Object> doMerge(final List<Object> oldValues, final List<Object> newValues) {
494             return oldValues.isEmpty() ? newValues : oldValues;
495         }
496 
497     }
498 
499     private static final class Union extends Criteria {
500 
501         private static final long serialVersionUID = 1L;
502 
503         Union(final URI... properties) {
504             super(properties);
505         }
506 
507         @Override
508         List<Object> doMerge(final List<Object> oldValues, final List<Object> newValues) {
509             if (oldValues.isEmpty()) {
510                 return newValues;
511             } else if (newValues.isEmpty()) {
512                 return oldValues;
513             } else {
514                 final Set<Object> set = Sets.newLinkedHashSet();
515                 set.addAll(oldValues);
516                 set.addAll(newValues);
517                 return ImmutableList.copyOf(set);
518             }
519         }
520 
521     }
522 
523     private static final class Min extends Criteria {
524 
525         private static final long serialVersionUID = 1L;
526 
527         Min(final URI... properties) {
528             super(properties);
529         }
530 
531         @Override
532         List<Object> doMerge(final List<Object> oldValues, final List<Object> newValues) {
533             if (oldValues.isEmpty()) {
534                 return newValues.size() <= 1 ? newValues : ImmutableList
535                         .of(((Ordering<Object>) Data.getTotalComparator()).min(newValues));
536             } else if (newValues.isEmpty()) {
537                 return oldValues.size() <= 1 ? oldValues : ImmutableList
538                         .of(((Ordering<Object>) Data.getTotalComparator()).min(oldValues));
539             } else {
540                 return ImmutableList.of(((Ordering<Object>) Data.getTotalComparator())
541                         .min(Iterables.concat(oldValues, newValues)));
542             }
543         }
544 
545     }
546 
547     private static final class Max extends Criteria {
548 
549         private static final long serialVersionUID = 1L;
550 
551         Max(final URI... properties) {
552             super(properties);
553         }
554 
555         @Override
556         List<Object> doMerge(final List<Object> oldValues, final List<Object> newValues) {
557             if (oldValues.isEmpty()) {
558                 return newValues.size() <= 1 ? newValues : ImmutableList
559                         .of(((Ordering<Object>) Data.getTotalComparator()).max(newValues));
560             } else if (newValues.isEmpty()) {
561                 return oldValues.size() <= 1 ? oldValues : ImmutableList
562                         .of(((Ordering<Object>) Data.getTotalComparator()).max(oldValues));
563             } else {
564                 return ImmutableList.of(((Ordering<Object>) Data.getTotalComparator())
565                         .max(Iterables.concat(oldValues, newValues)));
566             }
567         }
568 
569     }
570 
571     private static final class Compose extends Criteria {
572 
573         private static final long serialVersionUID = 1L;
574 
575         private final Criteria[] specificCriteria;
576 
577         private final Criteria defaultCriteria;
578 
579         Compose(final Criteria... criteria) {
580             super(extractProperties(criteria));
581             Criteria candidateDefaultCriteria = null;
582             final ImmutableList.Builder<Criteria> builder = ImmutableList.builder();
583             for (final Criteria c : criteria) {
584                 for (final Criteria d : c.decompose()) {
585                     if (!d.appliesToAll()) {
586                         builder.add(d);
587                     } else if (candidateDefaultCriteria == null) {
588                         candidateDefaultCriteria = d;
589                     }
590                 }
591             }
592             this.specificCriteria = Iterables.toArray(builder.build(), Criteria.class);
593             this.defaultCriteria = candidateDefaultCriteria;
594         }
595 
596         @Override
597         List<Object> doMerge(final URI property, final List<Object> oldValues,
598                 final List<Object> newValues) {
599             for (final Criteria c : this.specificCriteria) {
600                 if (c.appliesTo(property)) {
601                     return c.doMerge(oldValues, newValues);
602                 }
603             }
604             if (this.defaultCriteria != null) {
605                 return this.defaultCriteria.doMerge(oldValues, newValues);
606             }
607             return oldValues;
608         }
609 
610         @Override
611         List<Criteria> doDecompose() {
612             final ImmutableList.Builder<Criteria> builder = ImmutableList.builder();
613             builder.add(this.specificCriteria);
614             builder.add(this.defaultCriteria);
615             return builder.build();
616         }
617 
618         @Override
619         void doToString(final StringBuilder builder, //
620                 @Nullable final Map<String, String> namespaces) {
621             String separator = "";
622             for (final Criteria c : this.specificCriteria) {
623                 builder.append(separator);
624                 c.doToString(builder, namespaces);
625                 separator = ", ";
626             }
627             if (this.defaultCriteria != null) {
628                 builder.append(separator);
629                 this.defaultCriteria.doToString(builder, namespaces);
630             }
631         }
632 
633         private static URI[] extractProperties(final Criteria... criteria) {
634             final List<URI> properties = Lists.newArrayList();
635             for (final Criteria c : criteria) {
636                 properties.addAll(c.properties);
637             }
638             return properties.toArray(new URI[properties.size()]);
639         }
640 
641     }
642 
643 }
644 
645 // alternative API can be:
646 // (1) new Criteria().update(DC.TITLE).override(DC.ISSUED)
647 // - modifiable, verbose if a single option is used
648 // (2) Criteria.builder().update(DC.TITLE).override(DC.ISSUED).build()
649 // - verbose, esp if a single option is used
650 // alternative (2) can be however merged into this implementation, by adding a builder