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