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