View Javadoc
1   /**
2    * Copyright (C) 2014-2017 Philip Helger (www.helger.com)
3    * philip[at]helger[dot]com
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    *         http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package com.helger.schematron.pure.preprocess;
18  
19  import javax.annotation.Nonnull;
20  import javax.annotation.Nullable;
21  import javax.annotation.concurrent.NotThreadSafe;
22  
23  import com.helger.commons.ValueEnforcer;
24  import com.helger.commons.collection.ext.ICommonsList;
25  import com.helger.commons.collection.ext.ICommonsMap;
26  import com.helger.commons.collection.ext.ICommonsNavigableMap;
27  import com.helger.commons.string.ToStringGenerator;
28  import com.helger.schematron.pure.binding.IPSQueryBinding;
29  import com.helger.schematron.pure.model.IPSElement;
30  import com.helger.schematron.pure.model.PSActive;
31  import com.helger.schematron.pure.model.PSAssertReport;
32  import com.helger.schematron.pure.model.PSDiagnostic;
33  import com.helger.schematron.pure.model.PSDiagnostics;
34  import com.helger.schematron.pure.model.PSDir;
35  import com.helger.schematron.pure.model.PSEmph;
36  import com.helger.schematron.pure.model.PSExtends;
37  import com.helger.schematron.pure.model.PSLet;
38  import com.helger.schematron.pure.model.PSNS;
39  import com.helger.schematron.pure.model.PSName;
40  import com.helger.schematron.pure.model.PSPattern;
41  import com.helger.schematron.pure.model.PSPhase;
42  import com.helger.schematron.pure.model.PSRule;
43  import com.helger.schematron.pure.model.PSSchema;
44  import com.helger.schematron.pure.model.PSSpan;
45  import com.helger.schematron.pure.model.PSValueOf;
46  
47  /**
48   * This is the pre-processor class for pure Schematron. It converts an existing
49   * schema to the minimal syntax (by default) but allows for a certain degree of
50   * customization by keeping certain elements in the resulting schema. The actual
51   * query binding is used, so that report test expressions can be converted to
52   * assertions, and to replace the content of <param> elements into actual
53   * values.
54   *
55   * @author Philip Helger
56   */
57  @NotThreadSafe
58  public class PSPreprocessor
59  {
60    public static final boolean DEFAULT_KEEP_TITLES = false;
61    public static final boolean DEFAULT_KEEP_DIAGNOSTICS = false;
62    public static final boolean DEFAULT_KEEP_REPORTS = false;
63    public static final boolean DEFAULT_KEEP_EMPTY_PATTERNS = true;
64    public static final boolean DEFAULT_KEEP_EMPTY_SCHEMA = true;
65  
66    private final IPSQueryBinding m_aQueryBinding;
67    private boolean m_bKeepTitles = DEFAULT_KEEP_TITLES;
68    private boolean m_bKeepDiagnostics = DEFAULT_KEEP_DIAGNOSTICS;
69    private boolean m_bKeepReports = DEFAULT_KEEP_REPORTS;
70    private boolean m_bKeepEmptyPatterns = DEFAULT_KEEP_EMPTY_PATTERNS;
71    private boolean m_bKeepEmptySchema = DEFAULT_KEEP_EMPTY_SCHEMA;
72  
73    public PSPreprocessor (@Nonnull final IPSQueryBinding aQueryBinding)
74    {
75      m_aQueryBinding = ValueEnforcer.notNull (aQueryBinding, "QueryBinding");
76    }
77  
78    /**
79     * @return The query binding to be used. Never <code>null</code>!
80     */
81    @Nonnull
82    public IPSQueryBinding getQueryBinding ()
83    {
84      return m_aQueryBinding;
85    }
86  
87    /**
88     * @return <code>true</code> if &lt;title&gt;-elements should be kept. Default
89     *         is {@value #DEFAULT_KEEP_TITLES}.
90     */
91    public boolean isKeepTitles ()
92    {
93      return m_bKeepTitles;
94    }
95  
96    @Nonnull
97    public PSPreprocessor setKeepTitles (final boolean bKeepTitles)
98    {
99      m_bKeepTitles = bKeepTitles;
100     return this;
101   }
102 
103   /**
104    * @return <code>true</code> if &lt;diagnostics&gt;-elements should be kept.
105    *         Default is {@value #DEFAULT_KEEP_DIAGNOSTICS}.
106    */
107   public boolean isKeepDiagnostics ()
108   {
109     return m_bKeepDiagnostics;
110   }
111 
112   @Nonnull
113   public PSPreprocessor setKeepDiagnostics (final boolean bKeepDiagnostics)
114   {
115     m_bKeepDiagnostics = bKeepDiagnostics;
116     return this;
117   }
118 
119   /**
120    * @return <code>true</code> if &lt;report&gt;-elements should be kept.
121    *         Default is {@value #DEFAULT_KEEP_REPORTS}.
122    */
123   public boolean isKeepReports ()
124   {
125     return m_bKeepReports;
126   }
127 
128   @Nonnull
129   public PSPreprocessor setKeepReports (final boolean bKeepReports)
130   {
131     m_bKeepReports = bKeepReports;
132     return this;
133   }
134 
135   /**
136    * @return <code>true</code> if &lt;pattern&gt;-elements without a rule should
137    *         be kept. Default is {@value #DEFAULT_KEEP_EMPTY_PATTERNS}.
138    */
139   public boolean isKeepEmptyPatterns ()
140   {
141     return m_bKeepEmptyPatterns;
142   }
143 
144   @Nonnull
145   public PSPreprocessor setKeepEmptyPatterns (final boolean bKeepEmptyPatterns)
146   {
147     m_bKeepEmptyPatterns = bKeepEmptyPatterns;
148     return this;
149   }
150 
151   /**
152    * @return <code>true</code> if &lt;schema&gt;-elements without a pattern
153    *         should be kept. Default is {@value #DEFAULT_KEEP_EMPTY_SCHEMA}.
154    */
155   public boolean isKeepEmptySchema ()
156   {
157     return m_bKeepEmptySchema;
158   }
159 
160   /**
161    * Should schema objects without a pattern be kept? It makes only sense to set
162    * it to <code>false</code> if {@link #setKeepEmptyPatterns(boolean)} is also
163    * set to false, because otherwise patterns without rules are kept.
164    *
165    * @param bKeepEmptySchema
166    *        <code>true</code> to keep them, <code>false</code> to discard them.
167    * @return this
168    */
169   @Nonnull
170   public PSPreprocessor setKeepEmptySchema (final boolean bKeepEmptySchema)
171   {
172     m_bKeepEmptySchema = bKeepEmptySchema;
173     return this;
174   }
175 
176   @Nonnull
177   private static PSPhase _getPreprocessedPhase (@Nonnull final PSPhase aPhase,
178                                                 @Nonnull final PreprocessorIDPool aIDPool) throws SchematronPreprocessException
179   {
180     final PSPhase ret = new PSPhase ();
181     ret.setID (aIDPool.getUniqueID (aPhase.getID ()));
182     ret.setRich (aPhase.getRichClone ());
183     if (aPhase.hasAnyInclude ())
184       throw new SchematronPreprocessException ("Cannot preprocess <phase> with an <include>");
185     for (final IPSElement aElement : aPhase.getAllContentElements ())
186     {
187       if (aElement instanceof PSActive)
188         ret.addActive (((PSActive) aElement).getClone ());
189       else
190         if (aElement instanceof PSLet)
191           ret.addLet (((PSLet) aElement).getClone ());
192       // ps are ignored
193     }
194     ret.addForeignElements (aPhase.getAllForeignElements ());
195     ret.addForeignAttributes (aPhase.getAllForeignAttributes ());
196     return ret;
197   }
198 
199   /**
200    * Resolve all &lt;extends&gt; elements. This method calls itself recursively
201    * until all extends elements are resolved.
202    *
203    * @param aRuleContent
204    *        A list consisting of {@link PSAssertReport} and {@link PSExtends}
205    *        objects. Never <code>null</code>.
206    * @param aLookup
207    *        The rule lookup object
208    * @throws SchematronPreprocessException
209    *         If the base rule of an extends object could not be resolved.
210    */
211   private void _resolveRuleContent (@Nonnull final ICommonsList <IPSElement> aRuleContent,
212                                     @Nonnull final PreprocessorLookup aLookup,
213                                     @Nonnull final PreprocessorIDPool aIDPool,
214                                     @Nullable final ICommonsMap <String, String> aParamValueMap,
215                                     @Nonnull final PSRule aTargetRule) throws SchematronPreprocessException
216   {
217     for (final IPSElement aElement : aRuleContent)
218     {
219       if (aElement instanceof PSAssertReport)
220       {
221         final PSAssertReport aAssertReport = (PSAssertReport) aElement;
222         aTargetRule.addAssertReport (_getPreprocessedAssert (aAssertReport, aIDPool, aParamValueMap));
223       }
224       else
225       {
226         final PSExtends aExtends = (PSExtends) aElement;
227         final String sRuleID = aExtends.getRule ();
228         final PSRule aBaseRule = aLookup.getAbstractRuleOfID (sRuleID);
229         if (aBaseRule == null)
230           throw new SchematronPreprocessException ("Failed to resolve rule ID '" +
231                                                    sRuleID +
232                                                    "' in extends statement. Available rules are: " +
233                                                    aLookup.getAllAbstractRuleIDs ());
234 
235         // Recursively resolve the extends of the base rule
236         _resolveRuleContent (aBaseRule.getAllContentElements (), aLookup, aIDPool, aParamValueMap, aTargetRule);
237 
238         // Copy all lets
239         for (final PSLet aBaseLet : aBaseRule.getAllLets ())
240           aTargetRule.addLet (aBaseLet.getClone ());
241       }
242     }
243   }
244 
245   @Nonnull
246   private PSAssertReport _getPreprocessedAssert (@Nonnull final PSAssertReport aAssertReport,
247                                                  @Nonnull final PreprocessorIDPool aIDPool,
248                                                  @Nullable final ICommonsMap <String, String> aParamValueMap)
249   {
250     String sTest = aAssertReport.getTest ();
251     if (aAssertReport.isReport () && !m_bKeepReports)
252     {
253       // Negate the expression!
254       sTest = m_aQueryBinding.getNegatedTestExpression (sTest);
255     }
256 
257     // Keep report or make it always an assert
258     final PSAssertReport ret = new PSAssertReport (m_bKeepReports ? aAssertReport.isAssert () : true);
259     ret.setTest (m_aQueryBinding.getWithParamTextsReplaced (sTest, aParamValueMap));
260     ret.setFlag (aAssertReport.getFlag ());
261     ret.setID (aIDPool.getUniqueID (aAssertReport.getID ()));
262     if (m_bKeepDiagnostics)
263       ret.setDiagnostics (aAssertReport.getAllDiagnostics ());
264     ret.setRich (aAssertReport.getRichClone ());
265     ret.setLinkable (aAssertReport.getLinkableClone ());
266     for (final Object aContent : aAssertReport.getAllContentElements ())
267     {
268       if (aContent instanceof String)
269         ret.addText ((String) aContent);
270       else
271         if (aContent instanceof PSName)
272           ret.addName (((PSName) aContent).getClone ());
273         else
274           if (aContent instanceof PSValueOf)
275           {
276             final PSValueOf aValueOf = ((PSValueOf) aContent).getClone ();
277             aValueOf.setSelect (m_aQueryBinding.getWithParamTextsReplaced (aValueOf.getSelect (), aParamValueMap));
278             ret.addValueOf (aValueOf);
279           }
280           else
281             if (aContent instanceof PSEmph)
282               ret.addEmph (((PSEmph) aContent).getClone ());
283             else
284               if (aContent instanceof PSDir)
285                 ret.addDir (((PSDir) aContent).getClone ());
286               else
287                 if (aContent instanceof PSSpan)
288                   ret.addSpan (((PSSpan) aContent).getClone ());
289     }
290     ret.addForeignElements (aAssertReport.getAllForeignElements ());
291     ret.addForeignAttributes (aAssertReport.getAllForeignAttributes ());
292     return ret;
293   }
294 
295   @Nullable
296   private PSRule _getPreprocessedRule (@Nonnull final PSRule aRule,
297                                        @Nonnull final PreprocessorLookup aLookup,
298                                        @Nonnull final PreprocessorIDPool aIDPool,
299                                        @Nullable final ICommonsMap <String, String> aParamValueMap) throws SchematronPreprocessException
300   {
301     if (aRule.isAbstract ())
302     {
303       // Will be inlined
304       return null;
305     }
306 
307     final PSRule ret = new PSRule ();
308     ret.setFlag (aRule.getFlag ());
309     ret.setRich (aRule.getRichClone ());
310     ret.setLinkable (aRule.getLinkableClone ());
311     // abstract is always false
312     ret.setContext (m_aQueryBinding.getWithParamTextsReplaced (aRule.getContext (), aParamValueMap));
313     ret.setID (aIDPool.getUniqueID (aRule.getID ()));
314     if (aRule.hasAnyInclude ())
315       throw new SchematronPreprocessException ("Cannot preprocess <rule> with an <include>");
316     for (final PSLet aLet : aRule.getAllLets ())
317       ret.addLet (aLet.getClone ());
318     _resolveRuleContent (aRule.getAllContentElements (), aLookup, aIDPool, aParamValueMap, ret);
319     ret.addForeignElements (aRule.getAllForeignElements ());
320     ret.addForeignAttributes (aRule.getAllForeignAttributes ());
321     return ret;
322   }
323 
324   @Nullable
325   private PSPattern _getPreprocessedPattern (@Nonnull final PSPattern aPattern,
326                                              @Nonnull final PreprocessorLookup aLookup,
327                                              @Nonnull final PreprocessorIDPool aIDPool) throws SchematronPreprocessException
328   {
329     if (aPattern.isAbstract ())
330     {
331       // Will be inlined
332       return null;
333     }
334 
335     final PSPattern ret = new PSPattern ();
336     // abstract always false
337     // is-a must be resolved
338     ret.setID (aIDPool.getUniqueID (aPattern.getID ()));
339     ret.setRich (aPattern.getRichClone ());
340     if (aPattern.hasAnyInclude ())
341       throw new SchematronPreprocessException ("Cannot preprocess <pattern> with an <include>");
342     if (m_bKeepTitles && aPattern.hasTitle ())
343       ret.setTitle (aPattern.getTitle ().getClone ());
344 
345     final String sIsA = aPattern.getIsA ();
346     if (sIsA != null)
347     {
348       final PSPattern aBasePattern = aLookup.getAbstractPatternOfID (sIsA);
349       if (aBasePattern == null)
350         throw new SchematronPreprocessException ("Failed to resolve the pattern denoted by is-a='" + sIsA + "'");
351 
352       if (!ret.hasID ())
353         ret.setID (aIDPool.getUniqueID (aBasePattern.getID ()));
354       if (!ret.hasRich ())
355         ret.setRich (aBasePattern.getRichClone ());
356 
357       // get the string replacements
358       final ICommonsNavigableMap <String, String> aParamValueMap = m_aQueryBinding.getStringReplacementMap (aPattern.getAllParams ());
359 
360       for (final IPSElement aElement : aBasePattern.getAllContentElements ())
361       {
362         if (aElement instanceof PSLet)
363           ret.addLet (((PSLet) aElement).getClone ());
364         else
365           if (aElement instanceof PSRule)
366           {
367             final PSRule aMinifiedRule = _getPreprocessedRule ((PSRule) aElement, aLookup, aIDPool, aParamValueMap);
368             if (aMinifiedRule != null)
369               ret.addRule (aMinifiedRule);
370           }
371         // params must have be resolved
372         // ps are ignored
373       }
374     }
375     else
376     {
377       for (final IPSElement aElement : aPattern.getAllContentElements ())
378       {
379         if (aElement instanceof PSLet)
380           ret.addLet (((PSLet) aElement).getClone ());
381         else
382           if (aElement instanceof PSRule)
383           {
384             final PSRule aMinifiedRule = _getPreprocessedRule ((PSRule) aElement, aLookup, aIDPool, null);
385             if (aMinifiedRule != null)
386               ret.addRule (aMinifiedRule);
387           }
388         // params must be resolved
389         // ps are ignored
390       }
391     }
392     ret.addForeignElements (aPattern.getAllForeignElements ());
393     ret.addForeignAttributes (aPattern.getAllForeignAttributes ());
394     return ret;
395   }
396 
397   @Nonnull
398   private static PSDiagnostics _getPreprocessedDiagnostics (@Nonnull final PSDiagnostics aDiagnostics) throws SchematronPreprocessException
399   {
400     final PSDiagnostics ret = new PSDiagnostics ();
401     if (aDiagnostics.hasAnyInclude ())
402       throw new SchematronPreprocessException ("Cannot preprocess <diagnostics> with an <include>");
403     for (final PSDiagnostic aDiagnostic : aDiagnostics.getAllDiagnostics ())
404       ret.addDiagnostic (aDiagnostic.getClone ());
405     ret.addForeignElements (aDiagnostics.getAllForeignElements ());
406     ret.addForeignAttributes (aDiagnostics.getAllForeignAttributes ());
407     return ret;
408   }
409 
410   /**
411    * Convert the passed schema to a minimal schema.
412    *
413    * @param aSchema
414    *        The schema to be made minimal. May not be <code>null</code>
415    * @return The original schema object, if it is already minimal - a minimal
416    *         copy otherwise! May be <code>null</code> if the original schema is
417    *         not yet minimal and {@link #isKeepEmptySchema()} is set to
418    *         <code>false</code>.
419    * @throws SchematronPreprocessException
420    *         In case a preprocessing error occurs
421    */
422   @Nullable
423   public PSSchema getAsMinimalSchema (@Nonnull final PSSchema aSchema) throws SchematronPreprocessException
424   {
425     ValueEnforcer.notNull (aSchema, "Schema");
426 
427     // Anything to do?
428     if (aSchema.isMinimal ())
429       return aSchema;
430 
431     return getForcedPreprocessedSchema (aSchema);
432   }
433 
434   /**
435    * Convert the passed schema to a pre-processed schema.
436    *
437    * @param aSchema
438    *        The schema to pre-process. May not be <code>null</code>
439    * @return The original schema object, if it is already pre-processed - a
440    *         pre-processed copy otherwise! May be <code>null</code> if the
441    *         original schema is not yet pre-processed and
442    *         {@link #isKeepEmptySchema()} is set to <code>false</code>.
443    * @throws SchematronPreprocessException
444    *         In case a preprocessing error occurs
445    */
446   @Nullable
447   public PSSchema getAsPreprocessedSchema (@Nonnull final PSSchema aSchema) throws SchematronPreprocessException
448   {
449     ValueEnforcer.notNull (aSchema, "Schema");
450 
451     // Anything to do?
452     if (aSchema.isPreprocessed ())
453       return aSchema;
454 
455     return getForcedPreprocessedSchema (aSchema);
456   }
457 
458   /**
459    * Convert the passed schema to a pre-processed schema independent if it is
460    * already minimal or not.
461    *
462    * @param aSchema
463    *        The schema to be made minimal. May not be <code>null</code>
464    * @return A minimal copy of the schema. May be <code>null</code> if the
465    *         original schema is not yet minimal and {@link #isKeepEmptySchema()}
466    *         is set to <code>false</code>.
467    * @throws SchematronPreprocessException
468    *         In case a preprocessing error occurs
469    */
470   @Nullable
471   public PSSchema getForcedPreprocessedSchema (@Nonnull final PSSchema aSchema) throws SchematronPreprocessException
472   {
473     ValueEnforcer.notNull (aSchema, "Schema");
474 
475     final PreprocessorLookup aLookup = new PreprocessorLookup (aSchema);
476     final PreprocessorIDPool aIDPool = new PreprocessorIDPool ();
477 
478     final PSSchema ret = new PSSchema (aSchema.getResource ());
479     ret.setID (aIDPool.getUniqueID (aSchema.getID ()));
480     ret.setRich (aSchema.getRichClone ());
481     ret.setSchemaVersion (aSchema.getSchemaVersion ());
482     ret.setDefaultPhase (aSchema.getDefaultPhase ());
483     ret.setQueryBinding (aSchema.getQueryBinding ());
484     if (m_bKeepTitles && aSchema.hasTitle ())
485       ret.setTitle (aSchema.getTitle ().getClone ());
486     if (aSchema.hasAnyInclude ())
487       throw new SchematronPreprocessException ("Cannot preprocess <schema> with an <include>");
488     for (final PSNS aNS : aSchema.getAllNSs ())
489       ret.addNS (aNS.getClone ());
490     // start ps are skipped
491     for (final PSLet aLet : aSchema.getAllLets ())
492       ret.addLet (aLet.getClone ());
493     for (final PSPhase aPhase : aSchema.getAllPhases ())
494       ret.addPhase (_getPreprocessedPhase (aPhase, aIDPool));
495     for (final PSPattern aPattern : aSchema.getAllPatterns ())
496     {
497       final PSPattern aMinifiedPattern = _getPreprocessedPattern (aPattern, aLookup, aIDPool);
498       if (aMinifiedPattern != null)
499       {
500         // Pattern without rules?
501         if (aMinifiedPattern.getRuleCount () > 0 || m_bKeepEmptyPatterns)
502           ret.addPattern (aMinifiedPattern);
503       }
504     }
505 
506     // Schema without patterns?
507     if (aSchema.getPatternCount () == 0 && !m_bKeepEmptySchema)
508       return null;
509 
510     // end ps are skipped
511     if (m_bKeepDiagnostics && aSchema.hasDiagnostics ())
512       ret.setDiagnostics (_getPreprocessedDiagnostics (aSchema.getDiagnostics ()));
513     ret.addForeignElements (aSchema.getAllForeignElements ());
514     ret.addForeignAttributes (aSchema.getAllForeignAttributes ());
515     return ret;
516   }
517 
518   @Override
519   public String toString ()
520   {
521     return new ToStringGenerator (this).append ("queryBinding", m_aQueryBinding)
522                                        .append ("keepTitles", m_bKeepTitles)
523                                        .append ("keepDiagnostics", m_bKeepDiagnostics)
524                                        .append ("keepReports", m_bKeepReports)
525                                        .append ("keepEmptyPatterns", m_bKeepEmptyPatterns)
526                                        .append ("keepEmptySchema", m_bKeepEmptySchema)
527                                        .getToString ();
528   }
529 
530   @Nonnull
531   public static PSPreprocessor createPreprocessorWithoutInformationLoss (@Nonnull final IPSQueryBinding aQueryBinding)
532   {
533     final PSPreprocessor aPreprocessor = new PSPreprocessor (aQueryBinding);
534 
535     // Keep as much of the original information as possible, as it is not our
536     // goal to minify the scheme
537     aPreprocessor.setKeepReports (true);
538     aPreprocessor.setKeepDiagnostics (true);
539     aPreprocessor.setKeepTitles (true);
540 
541     return aPreprocessor;
542   }
543 }