1   /*
2    * RDFpro - An extensible tool for building stream-oriented RDF processing libraries.
3    * 
4    * Written in 2015 by Francesco Corcoglioniti with support by Alessio Palmero Aprosio and Marco
5    * Rospocher. Contact info on http://rdfpro.fbk.eu/
6    * 
7    * To the extent possible under law, the authors have dedicated all copyright and related and
8    * neighboring rights to this software to the public domain worldwide. This software is
9    * distributed without any warranty.
10   * 
11   * You should have received a copy of the CC0 Public Domain Dedication along with this software.
12   * If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
13   */
14  package eu.fbk.rdfpro;
15  
16  import java.util.ArrayList;
17  import java.util.Collection;
18  import java.util.Collections;
19  import java.util.List;
20  import java.util.Map;
21  import java.util.Objects;
22  import java.util.Set;
23  import java.util.concurrent.ConcurrentHashMap;
24  
25  import javax.annotation.Nullable;
26  
27  import com.google.common.collect.ImmutableMap;
28  import com.google.common.collect.ImmutableSet;
29  import com.google.common.collect.Lists;
30  import com.google.common.collect.Ordering;
31  import com.google.common.collect.Sets;
32  import com.google.common.hash.BloomFilter;
33  import com.google.common.hash.Funnels;
34  
35  import org.openrdf.model.Statement;
36  import org.openrdf.model.URI;
37  import org.openrdf.model.ValueFactory;
38  import org.openrdf.model.vocabulary.RDF;
39  import org.openrdf.query.BindingSet;
40  import org.openrdf.query.algebra.StatementPattern;
41  import org.openrdf.query.algebra.TupleExpr;
42  import org.openrdf.query.algebra.Var;
43  import org.slf4j.Logger;
44  import org.slf4j.LoggerFactory;
45  
46  import eu.fbk.rdfpro.util.Algebra;
47  import eu.fbk.rdfpro.util.Environment;
48  import eu.fbk.rdfpro.util.IO;
49  import eu.fbk.rdfpro.util.Namespaces;
50  import eu.fbk.rdfpro.util.QuadModel;
51  import eu.fbk.rdfpro.util.Statements;
52  import eu.fbk.rdfpro.vocab.RR;
53  
54  /**
55   * A set of rules, with associated optional meta-vocabulary.
56   */
57  public final class Ruleset {
58  
59      private static final Logger LOGGER = LoggerFactory.getLogger(Ruleset.class);
60  
61      public static final Ruleset RHODF = fromRDF(Environment.getProperty("rdfpro.rules.rhodf"));
62  
63      public static final Ruleset RDFS = fromRDF(Environment.getProperty("rdfpro.rules.rdfs"));
64  
65      public static final Ruleset OWL2RL = fromRDF(Environment.getProperty("rdfpro.rules.owl2rl"));
66  
67      private final Set<Rule> rules;
68  
69      private final Set<URI> metaVocabularyTerms;
70  
71      @Nullable
72      private transient Map<URI, Rule> ruleIndex;
73  
74      @Nullable
75      private transient List<RuleSplit> ruleSplits;
76  
77      private transient int hash;
78  
79      @Nullable
80      private transient Boolean deletePossible;
81  
82      @Nullable
83      private transient Boolean insertPossible;
84  
85      @Nullable
86      private transient BloomFilter<Integer>[] filters;
87  
88      /**
89       * Creates a new ruleset with the rules and meta-vocabulary terms supplied.
90       *
91       * @param rules
92       *            the rules
93       * @param metaVocabularyTerms
94       *            the meta-vocabulary terms
95       */
96      public Ruleset(final Iterable<Rule> rules, @Nullable final Iterable<URI> metaVocabularyTerms) {
97  
98          this.rules = ImmutableSet.copyOf(Ordering.natural().sortedCopy(rules));
99          this.metaVocabularyTerms = metaVocabularyTerms == null ? ImmutableSet.of() //
100                 : ImmutableSet.copyOf(Ordering.from(Statements.valueComparator()).sortedCopy(
101                         metaVocabularyTerms));
102 
103         this.ruleIndex = null;
104         this.ruleSplits = null;
105         this.hash = 0;
106         this.deletePossible = null;
107         this.insertPossible = null;
108         this.filters = null;
109     }
110 
111     /**
112      * Returns the rules in this ruleset.
113      *
114      * @return a set of rules sorted by phase index, fixpoint flag and rule URI.
115      */
116     public Set<Rule> getRules() {
117         return this.rules;
118     }
119 
120     /**
121      * Returns the rule with the ID specified, or null if there is no rule for that ID.
122      *
123      * @param ruleID
124      *            the rule ID
125      * @return the rule for the ID specified, or null if it does not exist
126      */
127     @Nullable
128     public Rule getRule(final URI ruleID) {
129         if (this.ruleIndex == null) {
130             final ImmutableMap.Builder<URI, Rule> builder = ImmutableMap.builder();
131             for (final Rule rule : this.rules) {
132                 builder.put(rule.getID(), rule);
133             }
134             this.ruleIndex = builder.build();
135         }
136         return this.ruleIndex.get(Objects.requireNonNull(ruleID));
137     }
138 
139     /**
140      * Returns the meta-vocabulary terms associated to this ruleset.
141      *
142      * @return a set of term URIs, sorted by URI
143      */
144     public Set<URI> getMetaVocabularyTerms() {
145         return this.metaVocabularyTerms;
146     }
147 
148     /**
149      * Returns true if the evaluation of the ruleset may cause some statements to be deleted. This
150      * happens if the ruleset contains at least a rule with a non-null DELETE expression.
151      *
152      * @return true, if statement deletion is possible with this ruleset
153      */
154     public boolean isDeletePossible() {
155         if (this.deletePossible == null) {
156             boolean deletePossible = false;
157             for (final Rule rule : this.rules) {
158                 if (rule.getDeleteExpr() != null) {
159                     deletePossible = true;
160                     break;
161                 }
162             }
163             this.deletePossible = deletePossible;
164         }
165         return this.deletePossible;
166     }
167 
168     /**
169      * Returs true if the evaluation of the ruleset may cause some statements to be inserted. This
170      * happens if the ruleset contains at least a rule with a non-null INSERT expression.
171      *
172      * @return true, if statement insertion is possible with this ruleset
173      */
174     public boolean isInsertPossible() {
175         if (this.insertPossible == null) {
176             boolean insertPossible = false;
177             for (final Rule rule : this.rules) {
178                 if (rule.getInsertExpr() != null) {
179                     insertPossible = true;
180                     break;
181                 }
182             }
183             this.insertPossible = insertPossible;
184         }
185         return this.insertPossible;
186     }
187 
188     /**
189      * Returns true if the supplied statement can be matched by a pattern in a WHERE or DELETE
190      * expression of some rule in this ruleset. Matchable statements (as defined before) are able
191      * to affect the rule evaluation process, whereas non-matchable statements can be safely
192      * removed with no effect on rule evaluation (i.e., no effect on the sets of statements
193      * deleted or inserted by the engine where evaluating this ruleset on some input data).
194      *
195      * @param stmt
196      *            the statement to test
197      * @return true, if the statement is matchable by some WHERE or DELETE rule expression
198      */
199     @SuppressWarnings("unchecked")
200     public boolean isMatchable(final Statement stmt) {
201 
202         // Initialize bloom filters at first access
203         if (this.filters == null) {
204             synchronized (this) {
205                 if (this.filters == null) {
206                     // Extract all statement patterns from WHERE and DELETE exprs
207                     final List<StatementPattern> patterns = new ArrayList<>();
208                     for (final Rule rule : this.rules) {
209                         patterns.addAll(rule.getDeletePatterns());
210                         patterns.addAll(rule.getWherePatterns());
211                     }
212 
213                     // Look for wildcard pattern
214                     BloomFilter<Integer>[] filters = new BloomFilter[] { null, null, null, null };
215                     for (final StatementPattern p : patterns) {
216                         if (!p.getSubjectVar().hasValue() && !p.getPredicateVar().hasValue()
217                                 && !p.getObjectVar().hasValue()
218                                 && (p.getContextVar() == null || !p.getContextVar().hasValue())) {
219                             filters = new BloomFilter[0];
220                             LOGGER.debug("Rules contain <?s ?p ?o ?c> pattern");
221                             break;
222                         }
223                     }
224 
225                     // Initialize SPOC bloom filters; null value = no constant in that position
226                     final int[] counts = new int[4];
227                     for (int i = 0; i < filters.length; ++i) {
228                         final Set<Integer> hashes = Sets.newHashSet();
229                         for (final StatementPattern pattern : patterns) {
230                             final List<Var> vars = pattern.getVarList();
231                             if (i < vars.size() && vars.get(i).hasValue()) {
232                                 hashes.add(vars.get(i).getValue().hashCode());
233                             }
234                         }
235                         counts[i] = hashes.size();
236                         if (!hashes.isEmpty()) {
237                             final BloomFilter<Integer> filter = BloomFilter.create(
238                                     Funnels.integerFunnel(), hashes.size());
239                             filters[i] = filter;
240                             for (final Integer hash : hashes) {
241                                 filter.put(hash);
242                             }
243 
244                         }
245                     }
246 
247                     // Atomically store the constructed filter list
248                     this.filters = filters;
249                     if (LOGGER.isDebugEnabled()) {
250                         LOGGER.debug("Number of constants in pattern components: "
251                                 + "s = {}, p = {}, o = {}, c = {}", counts[0], counts[1],
252                                 counts[2], counts[3]);
253                     }
254                 }
255             }
256         }
257 
258         // All statements matchable in case a wildcard <?s ?p ?o ?c> is present
259         if (this.filters.length == 0) {
260             return true;
261         }
262 
263         // Otherwise, at least a component of the statement must match one of the filter constants
264         final BloomFilter<Integer> subjFilter = this.filters[0];
265         final BloomFilter<Integer> predFilter = this.filters[1];
266         final BloomFilter<Integer> objFilter = this.filters[2];
267         final BloomFilter<Integer> ctxFilter = this.filters[3];
268         return subjFilter != null && subjFilter.mightContain(stmt.getSubject().hashCode()) //
269                 || predFilter != null && predFilter.mightContain(stmt.getPredicate().hashCode()) //
270                 || objFilter != null && objFilter.mightContain(stmt.getObject().hashCode()) //
271                 || ctxFilter != null && ctxFilter.mightContain(stmt.getContext() == null ? 0 : //
272                         stmt.getContext().hashCode());
273     }
274 
275     /**
276      * Returns the ruleset with the ABox rules obtained from the rules of this ruleset and the
277      * TBox data specified. The method split the DELETE, INSERT and WHERE expressions of rules
278      * into TBox and ABox parts. Rules with only TBox parts are discarded. Rules with only ABox
279      * parts are included as is in the returned ruleset. Rules with both ABox part (in DELETE
280      * and/or INSERT expressions) and TBox part (in WHERE expression) are instead 'exploded' by
281      * computing all the possible bindings of the TBox part w.r.t. the supplied TBox data, adding
282      * to the resulting ruleset all the rules obtained by injecting those bindings. Note: this
283      * method does not modify in any way the supplied TBox data. In general, it is necessary to
284      * close it w.r.t. relevant inference rules (which can be the same rules of this ruleset)
285      * before computing the ABox ruleset. In this case, the closure should be computed manually
286      * before calling this method.
287      *
288      * @param tboxData
289      *            the TBox data
290      * @return the resulting ruleset
291      */
292     public Ruleset getABoxRuleset(final QuadModel tboxData) {
293 
294         // Split rules if necessary, caching the result
295         if (this.ruleSplits == null) {
296             final List<RuleSplit> splits = new ArrayList<>(this.rules.size());
297             for (final Rule rule : this.rules) {
298                 splits.add(new RuleSplit(rule, this.metaVocabularyTerms));
299             }
300             this.ruleSplits = splits;
301         }
302 
303         // Compute preprocessing rules that obtain bindings of TBox WHERE exprs
304         final Map<URI, List<BindingSet>> bindingsMap = new ConcurrentHashMap<>();
305         final List<Runnable> queries = new ArrayList<>();
306         int numABoxRules = 0;
307         for (final RuleSplit split : this.ruleSplits) {
308             if (split.aboxDeleteExpr != null || split.aboxInsertExpr != null) {
309                 ++numABoxRules;
310                 if (split.tboxWhereExpr != null) {
311                     queries.add(new Runnable() {
312 
313                         @Override
314                         public void run() {
315                             final List<BindingSet> bindings = Lists.newArrayList(tboxData
316                                     .evaluate(split.tboxWhereExpr, null, null));
317                             bindingsMap.put(split.rule.getID(), bindings);
318                         }
319 
320                     });
321                 }
322             }
323         }
324         Environment.run(queries);
325 
326         // Compute the ABox rules using obtained bindings to explode the TBox WHERE parts
327         final List<Rule> rules = new ArrayList<>();
328         for (final RuleSplit split : this.ruleSplits) {
329             if (split.aboxDeleteExpr != null || split.aboxInsertExpr != null) {
330                 final URI id = split.rule.getID();
331                 final boolean fixpoint = split.rule.isFixpoint();
332                 final int phase = split.rule.getPhase();
333                 if (split.tboxWhereExpr == null) {
334                     final URI newID = Rule.newID(id.stringValue());
335                     rules.add(new Rule(newID, fixpoint, phase, split.aboxDeleteExpr,
336                             split.aboxInsertExpr, split.aboxWhereExpr));
337                 } else {
338                     final Iterable<? extends BindingSet> list = bindingsMap.get(id);
339                     if (list != null) {
340                         for (final BindingSet b : list) {
341                             final TupleExpr delete = Algebra.normalize(
342                                     Algebra.rewrite(split.aboxDeleteExpr, b),
343                                     Statements.VALUE_NORMALIZER);
344                             final TupleExpr insert = Algebra.normalize(
345                                     Algebra.rewrite(split.aboxInsertExpr, b),
346                                     Statements.VALUE_NORMALIZER);
347                             final TupleExpr where = Algebra.normalize(
348                                     Algebra.rewrite(split.aboxWhereExpr, b),
349                                     Statements.VALUE_NORMALIZER);
350                             if (!Objects.equals(insert, where) || delete != null) {
351                                 final URI newID = Rule.newID(id.stringValue());
352                                 rules.add(new Rule(newID, fixpoint, phase, delete, insert, where));
353                             }
354                         }
355                     }
356                 }
357             }
358         }
359         LOGGER.debug("{} ABox rules derived from {} TBox quads and {} original rules "
360                 + "({} with ABox components, {} with TBox & ABox components)", rules.size(),
361                 tboxData.size(), this.rules.size(), numABoxRules, queries.size());
362 
363         // Build and return the resulting ruleset
364         return new Ruleset(rules, this.metaVocabularyTerms);
365     }
366 
367     /**
368      * Returns the ruleset obtained by rewriting the rules of this ruleset according to the GLOBAL
369      * graph inference mode, using the global graph URI specified. Meta-vocabulary terms are not
370      * affected.
371      *
372      *
373      * @param globalGraph
374      *            the URI of the global graph where to insert new quads; if null, quads will be
375      *            inserted in the default graph {@code sesame:nil}
376      * @return a ruleset with the rewritten rules and the same meta-vocabulary terms of this
377      *         ruleset
378      * @see Rule#rewriteGlobalGM(URI)
379      */
380     public Ruleset rewriteGlobalGM(@Nullable final URI globalGraph) {
381         final List<Rule> rewrittenRules = new ArrayList<>();
382         for (final Rule rule : this.rules) {
383             rewrittenRules.add(rule.rewriteGlobalGM(globalGraph));
384         }
385         return new Ruleset(rewrittenRules, this.metaVocabularyTerms);
386     }
387 
388     /**
389      * Returns the ruleset obtained by rewriting the rules of this ruleset according to the
390      * SEPARATE graph inference mode. Meta-vocabulary terms are not affected.
391      *
392      * @return a ruleset with the rewritten rules and the same meta-vocabulary terms of this
393      *         ruleset
394      * @see Rule#rewriteSeparateGM()
395      */
396     public Ruleset rewriteSeparateGM() {
397         final List<Rule> rewrittenRules = new ArrayList<>();
398         for (final Rule rule : this.rules) {
399             rewrittenRules.add(rule.rewriteSeparateGM());
400         }
401         return new Ruleset(rewrittenRules, this.metaVocabularyTerms);
402     }
403 
404     /**
405      * Returns the ruleset obtained by rewriting the rules of this ruleset according to the STAR
406      * graph inference mode, using the global graph URI supplied. Meta-vocabulary terms are not
407      * affected.
408      *
409      * @param globalGraph
410      *            the URI of the global graph whose quads are 'imported' in other graphs; if null,
411      *            the default graph {@code sesame:nil} will be used
412      * @return a ruleset with the rewritten rules and the same meta-vocabulary terms of this
413      *         ruleset
414      * @see Rule#rewriteStarGM(URI)
415      */
416     public Ruleset rewriteStarGM(@Nullable final URI globalGraph) {
417         final List<Rule> rewrittenRules = new ArrayList<>();
418         for (final Rule rule : this.rules) {
419             rewrittenRules.add(rule.rewriteStarGM(globalGraph));
420         }
421         return new Ruleset(rewrittenRules, this.metaVocabularyTerms);
422     }
423 
424     /**
425      * Returns the ruleset obtained by replacing selected variables in the rules of this ruleset
426      * with the constant values dictated by the supplied bindings. Meta-vocabulary terms are not
427      * affected.
428      *
429      * @param bindings
430      *            the variable = value bindings to use for rewriting rules; if null or empty, no
431      *            rewriting will take place
432      * @return a ruleset with the rewritten rules and the same meta-vocabulary terms of this
433      *         ruleset
434      * @see Rule#rewriteVariables(BindingSet)
435      */
436     public Ruleset rewriteVariables(@Nullable final BindingSet bindings) {
437         if (bindings == null || bindings.size() == 0) {
438             return this;
439         }
440         final List<Rule> rewrittenRules = new ArrayList<>();
441         for (final Rule rule : this.rules) {
442             rewrittenRules.add(rule.rewriteVariables(bindings));
443         }
444         return new Ruleset(rewrittenRules, this.metaVocabularyTerms);
445     }
446 
447     /**
448      * Returns the ruleset obtained by merging the rules in this ruleset with the same WHERE
449      * expression, priority and fixpoint flag. Meta-vocabulary terms are not affected.
450      *
451      * @return a ruleset with the merged rules and the same meta-vocabulary terms of this ruleset
452      * @see Rule#mergeSameWhereExpr(Iterable)
453      */
454     public Ruleset mergeSameWhereExpr() {
455         final List<Rule> rules = Rule.mergeSameWhereExpr(this.rules);
456         return rules.size() == this.rules.size() ? this : new Ruleset(rules,
457                 this.metaVocabularyTerms);
458     }
459 
460     /**
461      * {@inheritDoc} Two rulesets are equal if they have the same rules and meta-vocabulary terms.
462      */
463     @Override
464     public boolean equals(final Object object) {
465         if (object == this) {
466             return true;
467         }
468         if (!(object instanceof Ruleset)) {
469             return false;
470         }
471         final Ruleset other = (Ruleset) object;
472         return this.rules.equals(other.rules)
473                 && this.metaVocabularyTerms.equals(other.metaVocabularyTerms);
474     }
475 
476     /**
477      * {@inheritDoc} The returned hash code depends on all the rules and meta-vocabulary terms in
478      * this ruleset.
479      */
480     @Override
481     public int hashCode() {
482         if (this.hash == 0) {
483             this.hash = Objects.hash(this.rules, this.metaVocabularyTerms);
484         }
485         return this.hash;
486     }
487 
488     /**
489      * {@inheritDoc} The returned string lists, on multiple lines, all the meta-vocabulary terms
490      * and rules in this ruleset.
491      */
492     @Override
493     public String toString() {
494         final StringBuilder builder = new StringBuilder();
495         builder.append("META-VOCABULARY TERMS (").append(this.metaVocabularyTerms.size())
496                 .append("):");
497         for (final URI metaVocabularyTerm : this.metaVocabularyTerms) {
498             builder.append("\n").append(
499                     Statements.formatValue(metaVocabularyTerm, Namespaces.DEFAULT));
500         }
501         builder.append("\n\nRULES (").append(this.rules.size()).append("):");
502         for (final Rule rule : this.rules) {
503             builder.append("\n").append(rule);
504         }
505         return builder.toString();
506     }
507 
508     /**
509      * Emits the RDF serialization of the ruleset. Emitted triples are placed in the default
510      * graph.
511      *
512      * @param output
513      *            the collection where to add emitted RDF statements, not null
514      * @return the supplied collection
515      */
516     public <T extends Collection<? super Statement>> T toRDF(final T output) {
517 
518         // Emit meta-vocabulary terms
519         final ValueFactory vf = Statements.VALUE_FACTORY;
520         for (final URI metaVocabularyTerm : this.metaVocabularyTerms) {
521             vf.createStatement(metaVocabularyTerm, RDF.TYPE, RR.META_VOCABULARY_TERM);
522         }
523 
524         // Emit rules
525         for (final Rule rule : this.rules) {
526             rule.toRDF(output);
527         }
528         return output;
529     }
530 
531     /**
532      * Parses a ruleset from the supplied RDF statements. The method extracts all the rules and
533      * the meta-vocabulary terms defined by supplied statements, and collects them in a new
534      * ruleset.
535      *
536      * @param model
537      *            the RDF statements, not null
538      * @return the parsed ruleset
539      */
540     public static Ruleset fromRDF(final Iterable<Statement> model) {
541 
542         // Parse meta-vocabulary terms
543         final List<URI> metaVocabularyTerms = new ArrayList<>();
544         for (final Statement stmt : model) {
545             if (stmt.getSubject() instanceof URI && RDF.TYPE.equals(stmt.getPredicate())
546                     && RR.META_VOCABULARY_TERM.equals(stmt.getObject())) {
547                 metaVocabularyTerms.add((URI) stmt.getSubject());
548             }
549         }
550 
551         // Parse rules
552         final List<Rule> rules = Rule.fromRDF(model);
553 
554         // Build resulting ruleset
555         return new Ruleset(rules, metaVocabularyTerms);
556     }
557 
558     /**
559      * Parses a ruleset from the RDF located at the specified location. The method extracts all
560      * the rules and the meta-vocabulary terms defined in the RDF, and collects them in a new
561      * ruleset.
562      *
563      * @param location
564      *            the location where to load RDF data, not null
565      * @return the parsed ruleset
566      */
567     public static Ruleset fromRDF(final String location) {
568         final String url = IO.extractURL(location).toString();
569         final RDFSource rulesetSource = RDFSources.read(true, true, null, null, url);
570         return Ruleset.fromRDF(rulesetSource);
571     }
572 
573     /**
574      * Merges multiple rulesets in a single ruleset. The method collects all the rules and
575      * meta-vocabulary terms in the specified rulesets, and creates a new ruleset (if necessary)
576      * containing the resulting rules and terms.
577      *
578      * @param rulesets
579      *            the rulesets to merge
580      * @return the merged ruleset (possibly one of the input rulesets)
581      */
582     public static Ruleset merge(final Ruleset... rulesets) {
583         if (rulesets.length == 0) {
584             return new Ruleset(Collections.emptyList(), Collections.emptyList());
585         } else if (rulesets.length == 1) {
586             return rulesets[0];
587         } else {
588             final List<URI> metaVocabularyTerms = new ArrayList<>();
589             final List<Rule> rules = new ArrayList<>();
590             for (final Ruleset ruleset : rulesets) {
591                 metaVocabularyTerms.addAll(ruleset.getMetaVocabularyTerms());
592                 rules.addAll(ruleset.getRules());
593             }
594             return new Ruleset(rules, metaVocabularyTerms);
595         }
596     }
597 
598     private static final class RuleSplit {
599 
600         final Rule rule;
601 
602         @Nullable
603         final TupleExpr tboxDeleteExpr;
604 
605         @Nullable
606         final TupleExpr aboxDeleteExpr;
607 
608         @Nullable
609         final TupleExpr tboxInsertExpr;
610 
611         @Nullable
612         final TupleExpr aboxInsertExpr;
613 
614         @Nullable
615         final TupleExpr tboxWhereExpr;
616 
617         @Nullable
618         final TupleExpr aboxWhereExpr;
619 
620         RuleSplit(final Rule rule, final Set<URI> terms) {
621             try {
622                 final TupleExpr[] deleteExprs = Algebra.splitTupleExpr(rule.getDeleteExpr(),
623                         terms, -1);
624                 final TupleExpr[] insertExprs = Algebra.splitTupleExpr(rule.getInsertExpr(),
625                         terms, -1);
626                 final TupleExpr[] whereExprs = Algebra.splitTupleExpr(
627                         Algebra.explodeFilters(rule.getWhereExpr()), terms, 1);
628 
629                 this.rule = rule;
630                 this.tboxDeleteExpr = deleteExprs[0];
631                 this.aboxDeleteExpr = deleteExprs[1];
632                 this.tboxInsertExpr = insertExprs[0];
633                 this.aboxInsertExpr = insertExprs[1];
634                 this.tboxWhereExpr = whereExprs[0];
635                 this.aboxWhereExpr = whereExprs[1];
636 
637                 LOGGER.trace("{}", this);
638 
639             } catch (final Throwable ex) {
640                 throw new IllegalArgumentException("Cannot split rule " + rule.getID(), ex);
641             }
642         }
643 
644         @Override
645         public String toString() {
646             final StringBuilder builder = new StringBuilder();
647             builder.append("Splitting of rule ").append(this.rule.getID());
648             toStringHelper(builder, "\n  DELETE original: ", this.rule.getDeleteExpr());
649             toStringHelper(builder, "\n  DELETE tbox:     ", this.tboxDeleteExpr);
650             toStringHelper(builder, "\n  DELETE abox:     ", this.aboxDeleteExpr);
651             toStringHelper(builder, "\n  INSERT original: ", this.rule.getInsertExpr());
652             toStringHelper(builder, "\n  INSERT tbox:     ", this.tboxInsertExpr);
653             toStringHelper(builder, "\n  INSERT abox:     ", this.aboxInsertExpr);
654             toStringHelper(builder, "\n  WHERE  original: ", this.rule.getWhereExpr());
655             toStringHelper(builder, "\n  WHERE  tbox:     ", this.tboxWhereExpr);
656             toStringHelper(builder, "\n  WHERE  abox:     ", this.aboxWhereExpr);
657             return builder.toString();
658         }
659 
660         private void toStringHelper(final StringBuilder builder, final String prefix,
661                 @Nullable final TupleExpr expr) {
662             if (expr != null) {
663                 builder.append(prefix).append(Algebra.format(expr));
664             }
665         }
666 
667     }
668 
669 }