View Javadoc
1   /**
2    * Copyright (C) 2014-2018 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.model;
18  
19  import java.util.Map;
20  
21  import javax.annotation.Nonnegative;
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.CollectionHelper;
29  import com.helger.commons.collection.impl.CommonsArrayList;
30  import com.helger.commons.collection.impl.CommonsLinkedHashMap;
31  import com.helger.commons.collection.impl.ICommonsList;
32  import com.helger.commons.collection.impl.ICommonsOrderedMap;
33  import com.helger.commons.io.resource.IReadableResource;
34  import com.helger.commons.string.StringHelper;
35  import com.helger.commons.string.ToStringGenerator;
36  import com.helger.schematron.CSchematron;
37  import com.helger.schematron.CSchematronXML;
38  import com.helger.schematron.pure.errorhandler.IPSErrorHandler;
39  import com.helger.xml.microdom.IMicroElement;
40  import com.helger.xml.microdom.MicroElement;
41  import com.helger.xml.namespace.MapBasedNamespaceContext;
42  
43  /**
44   * A single Schematron schema-element.<br>
45   * The top-level element of a Schematron schema.<br>
46   * The optional schemaVersion attribute gives the version of the schema. Its
47   * allowed values are not defined by this part of ISO/IEC 19757 and its use is
48   * implementation-dependent.<br>
49   * The optional queryBinding attribute provides the short name of the query
50   * language binding in use. If this attribute is specified, it is an error if it
51   * has a value that the current implementation does not support.<br>
52   * The defaultPhase attribute may be used to indicate the phase to use in the
53   * absence of explicit user-supplied information.<br>
54   * The title and p elements allow rich documentation.<br>
55   * The icon, see and fpi attributes allow rich interfaces and documentation.<br>
56   *
57   * @author Philip Helger
58   */
59  @NotThreadSafe
60  public class PSSchema implements
61                        IPSElement,
62                        IPSHasID,
63                        IPSHasForeignElements,
64                        IPSHasIncludes,
65                        IPSHasLets,
66                        IPSHasRichGroup
67  {
68    private final IReadableResource m_aResource;
69    private String m_sID;
70    private PSRichGroup m_aRich;
71    private String m_sSchemaVersion;
72    private String m_sDefaultPhase;
73    private String m_sQueryBinding;
74    private PSTitle m_aTitle;
75    private final ICommonsList <PSInclude> m_aIncludes = new CommonsArrayList <> ();
76    private final ICommonsList <PSNS> m_aNSs = new CommonsArrayList <> ();
77    private final ICommonsList <PSP> m_aStartPs = new CommonsArrayList <> ();
78    private final ICommonsList <PSLet> m_aLets = new CommonsArrayList <> ();
79    private final ICommonsList <PSPhase> m_aPhases = new CommonsArrayList <> ();
80    private final ICommonsList <PSPattern> m_aPatterns = new CommonsArrayList <> ();
81    private final ICommonsList <PSP> m_aEndPs = new CommonsArrayList <> ();
82    private PSDiagnostics m_aDiagnostics;
83    private ICommonsOrderedMap <String, String> m_aForeignAttrs;
84    private ICommonsList <IMicroElement> m_aForeignElements;
85  
86    /**
87     * Default constructor for a new schema that was not read from a file.
88     */
89    public PSSchema ()
90    {
91      this (null);
92    }
93  
94    /**
95     * Constructor for reading a schema from a file.
96     *
97     * @param aResource
98     *        The resource to be used. May be <code>null</code> indicating that
99     *        this is a newly created schema.
100    */
101   public PSSchema (@Nullable final IReadableResource aResource)
102   {
103     m_aResource = aResource;
104   }
105 
106   /**
107    * @return The resource from which the schema was read. May be
108    *         <code>null</code> if the schema is newly created.
109    */
110   @Nullable
111   public IReadableResource getResource ()
112   {
113     return m_aResource;
114   }
115 
116   public boolean isValid (@Nonnull final IPSErrorHandler aErrorHandler)
117   {
118     if (m_aPatterns.isEmpty ())
119     {
120       aErrorHandler.error (this, "<schema> has no <pattern>s");
121       return false;
122     }
123     if (m_aTitle != null && !m_aTitle.isValid (aErrorHandler))
124       return false;
125     for (final PSInclude aInclude : m_aIncludes)
126       if (!aInclude.isValid (aErrorHandler))
127         return false;
128     for (final PSNS aNS : m_aNSs)
129       if (!aNS.isValid (aErrorHandler))
130         return false;
131     for (final PSP aP : m_aStartPs)
132       if (!aP.isValid (aErrorHandler))
133         return false;
134     for (final PSLet aLet : m_aLets)
135       if (!aLet.isValid (aErrorHandler))
136         return false;
137     for (final PSPhase aPhase : m_aPhases)
138       if (!aPhase.isValid (aErrorHandler))
139         return false;
140     for (final PSPattern aPattern : m_aPatterns)
141       if (!aPattern.isValid (aErrorHandler))
142         return false;
143     for (final PSP aP : m_aEndPs)
144       if (!aP.isValid (aErrorHandler))
145         return false;
146     if (m_aDiagnostics != null && !m_aDiagnostics.isValid (aErrorHandler))
147       return false;
148     return true;
149   }
150 
151   public void validateCompletely (@Nonnull final IPSErrorHandler aErrorHandler)
152   {
153     if (m_aPatterns.isEmpty ())
154       aErrorHandler.error (this, "<schema> has no <pattern>s");
155     if (m_aTitle != null)
156       m_aTitle.validateCompletely (aErrorHandler);
157     for (final PSInclude aInclude : m_aIncludes)
158       aInclude.validateCompletely (aErrorHandler);
159     for (final PSNS aNS : m_aNSs)
160       aNS.validateCompletely (aErrorHandler);
161     for (final PSP aP : m_aStartPs)
162       aP.validateCompletely (aErrorHandler);
163     for (final PSLet aLet : m_aLets)
164       aLet.validateCompletely (aErrorHandler);
165     for (final PSPhase aPhase : m_aPhases)
166       aPhase.validateCompletely (aErrorHandler);
167     for (final PSPattern aPattern : m_aPatterns)
168       aPattern.validateCompletely (aErrorHandler);
169     for (final PSP aP : m_aEndPs)
170       aP.validateCompletely (aErrorHandler);
171     if (m_aDiagnostics != null)
172       m_aDiagnostics.validateCompletely (aErrorHandler);
173   }
174 
175   /**
176    * Check if this schema is already pre-processed or not. A schema is
177    * considered pre-processed if:
178    * <ul>
179    * <li>no includes are contained recursively and</li>
180    * <li>no abstract patterns are contained and</li>
181    * <li>no abstract rules are contained.</li>
182    * </ul>
183    *
184    * @return <code>true</code> if it is pre-processed, <code>false</code> if
185    *         not.
186    */
187   public boolean isPreprocessed ()
188   {
189     if (hasAnyInclude ())
190       return false;
191 
192     for (final PSPhase aPhase : m_aPhases)
193       if (aPhase.hasAnyInclude ())
194         return false;
195 
196     for (final PSPattern aPattern : m_aPatterns)
197     {
198       if (aPattern.isAbstract () || aPattern.hasAnyInclude () || aPattern.hasAnyParam ())
199         return false;
200       for (final PSRule aRule : aPattern.getAllRules ())
201       {
202         if (aRule.isAbstract () || aRule.hasAnyInclude () || aRule.hasAnyExtends ())
203           return false;
204       }
205     }
206 
207     if (m_aDiagnostics != null && m_aDiagnostics.hasAnyInclude ())
208       return false;
209     return true;
210   }
211 
212   public boolean isMinimal ()
213   {
214     if (m_aTitle != null && !m_aTitle.isMinimal ())
215       return false;
216     for (final PSInclude aInclude : m_aIncludes)
217       if (!aInclude.isMinimal ())
218         return false;
219     for (final PSNS aNS : m_aNSs)
220       if (!aNS.isMinimal ())
221         return false;
222     for (final PSP aP : m_aStartPs)
223       if (!aP.isMinimal ())
224         return false;
225     for (final PSLet aLet : m_aLets)
226       if (!aLet.isMinimal ())
227         return false;
228     for (final PSPhase aPhase : m_aPhases)
229       if (!aPhase.isMinimal ())
230         return false;
231     for (final PSPattern aPattern : m_aPatterns)
232       if (!aPattern.isMinimal ())
233         return false;
234     for (final PSP aP : m_aEndPs)
235       if (!aP.isMinimal ())
236         return false;
237     if (m_aDiagnostics != null && !m_aDiagnostics.isMinimal ())
238       return false;
239     return true;
240   }
241 
242   public void addForeignElement (@Nonnull final IMicroElement aForeignElement)
243   {
244     ValueEnforcer.notNull (aForeignElement, "ForeignElement");
245     if (aForeignElement.hasParent ())
246       throw new IllegalArgumentException ("ForeignElement already has a parent!");
247     if (m_aForeignElements == null)
248       m_aForeignElements = new CommonsArrayList <> ();
249     m_aForeignElements.add (aForeignElement);
250   }
251 
252   public boolean hasForeignElements ()
253   {
254     return m_aForeignElements != null && m_aForeignElements.isNotEmpty ();
255   }
256 
257   @Nonnull
258   @ReturnsMutableCopy
259   public ICommonsList <IMicroElement> getAllForeignElements ()
260   {
261     return new CommonsArrayList <> (m_aForeignElements);
262   }
263 
264   public void addForeignAttribute (@Nonnull final String sAttrName, @Nonnull final String sAttrValue)
265   {
266     ValueEnforcer.notNull (sAttrName, "AttrName");
267     ValueEnforcer.notNull (sAttrValue, "AttrValue");
268     if (m_aForeignAttrs == null)
269       m_aForeignAttrs = new CommonsLinkedHashMap <> ();
270     m_aForeignAttrs.put (sAttrName, sAttrValue);
271   }
272 
273   public boolean hasForeignAttributes ()
274   {
275     return m_aForeignAttrs != null && m_aForeignAttrs.isNotEmpty ();
276   }
277 
278   @Nonnull
279   @ReturnsMutableCopy
280   public ICommonsOrderedMap <String, String> getAllForeignAttributes ()
281   {
282     return new CommonsLinkedHashMap <> (m_aForeignAttrs);
283   }
284 
285   public void setID (@Nullable final String sID)
286   {
287     m_sID = sID;
288   }
289 
290   @Nullable
291   public String getID ()
292   {
293     return m_sID;
294   }
295 
296   public void setRich (@Nullable final PSRichGroup aRich)
297   {
298     m_aRich = aRich;
299   }
300 
301   @Nullable
302   public PSRichGroup getRich ()
303   {
304     return m_aRich;
305   }
306 
307   public void setQueryBinding (@Nullable final String sQueryBinding)
308   {
309     if (sQueryBinding != null && sQueryBinding.length () == 0)
310       throw new IllegalArgumentException ("queryBinding may not be empty!");
311     m_sQueryBinding = sQueryBinding;
312   }
313 
314   @Nullable
315   public String getQueryBinding ()
316   {
317     return m_sQueryBinding;
318   }
319 
320   public void setSchemaVersion (@Nullable final String sSchemaVersion)
321   {
322     if (sSchemaVersion != null && sSchemaVersion.length () == 0)
323       throw new IllegalArgumentException ("schemaVersion may not be empty!");
324     m_sSchemaVersion = sSchemaVersion;
325   }
326 
327   @Nullable
328   public String getSchemaVersion ()
329   {
330     return m_sSchemaVersion;
331   }
332 
333   public void setDefaultPhase (@Nullable final String sDefaultPhase)
334   {
335     m_sDefaultPhase = sDefaultPhase;
336   }
337 
338   @Nullable
339   public String getDefaultPhase ()
340   {
341     return m_sDefaultPhase;
342   }
343 
344   public void setTitle (@Nullable final PSTitle aTitle)
345   {
346     m_aTitle = aTitle;
347   }
348 
349   @Nullable
350   public PSTitle getTitle ()
351   {
352     return m_aTitle;
353   }
354 
355   public boolean hasTitle ()
356   {
357     return m_aTitle != null;
358   }
359 
360   public void addInclude (@Nonnull final PSInclude aInclude)
361   {
362     ValueEnforcer.notNull (aInclude, "Include");
363     m_aIncludes.add (aInclude);
364   }
365 
366   public boolean hasAnyInclude ()
367   {
368     return m_aIncludes.isNotEmpty ();
369   }
370 
371   @Nonnull
372   @ReturnsMutableCopy
373   public ICommonsList <PSInclude> getAllIncludes ()
374   {
375     return m_aIncludes.getClone ();
376   }
377 
378   public void addNS (@Nonnull final PSNS aNS)
379   {
380     ValueEnforcer.notNull (aNS, "NS");
381     m_aNSs.add (aNS);
382   }
383 
384   public boolean hasAnyNS ()
385   {
386     return m_aNSs.isNotEmpty ();
387   }
388 
389   @Nonnull
390   @ReturnsMutableCopy
391   public ICommonsList <PSNS> getAllNSs ()
392   {
393     return m_aNSs.getClone ();
394   }
395 
396   /**
397    * @return All contained namespaces as a single namespace context
398    */
399   @Nonnull
400   @ReturnsMutableCopy
401   public MapBasedNamespaceContext getAsNamespaceContext ()
402   {
403     final MapBasedNamespaceContext ret = new MapBasedNamespaceContext ();
404     for (final PSNS aNS : m_aNSs)
405       ret.addMapping (aNS.getPrefix (), aNS.getUri ());
406     return ret;
407   }
408 
409   public void addStartP (@Nonnull final PSP aP)
410   {
411     ValueEnforcer.notNull (aP, "P");
412     m_aStartPs.add (aP);
413   }
414 
415   @Nonnull
416   @ReturnsMutableCopy
417   public ICommonsList <PSP> getAllStartPs ()
418   {
419     return m_aStartPs.getClone ();
420   }
421 
422   public void addLet (@Nonnull final PSLet aLet)
423   {
424     ValueEnforcer.notNull (aLet, "Let");
425     m_aLets.add (aLet);
426   }
427 
428   public boolean hasAnyLet ()
429   {
430     return m_aLets.isNotEmpty ();
431   }
432 
433   @Nonnull
434   @ReturnsMutableCopy
435   public ICommonsList <PSLet> getAllLets ()
436   {
437     return m_aLets.getClone ();
438   }
439 
440   @Nonnull
441   @ReturnsMutableCopy
442   public ICommonsOrderedMap <String, String> getAllLetsAsMap ()
443   {
444     final ICommonsOrderedMap <String, String> ret = new CommonsLinkedHashMap <> ();
445     for (final PSLet aLet : m_aLets)
446       ret.put (aLet.getName (), aLet.getValue ());
447     return ret;
448   }
449 
450   public void addPhase (@Nonnull final PSPhase aPhase)
451   {
452     ValueEnforcer.notNull (aPhase, "Phase");
453     m_aPhases.add (aPhase);
454   }
455 
456   @Nonnull
457   @ReturnsMutableCopy
458   public ICommonsList <PSPhase> getAllPhases ()
459   {
460     return m_aPhases.getClone ();
461   }
462 
463   /**
464    * @return A list with all phase IDs. Only phases having a valid ID are
465    *         considered.
466    */
467   @Nonnull
468   @ReturnsMutableCopy
469   public ICommonsList <String> getAllPhaseIDs ()
470   {
471     return m_aPhases.getAllMapped (PSPhase::hasID, PSPhase::getID);
472   }
473 
474   @Nullable
475   public PSPhase getPhaseOfID (@Nullable final String sID)
476   {
477     if (StringHelper.hasText (sID))
478       for (final PSPhase aPhase : m_aPhases)
479         if (sID.equals (aPhase.getID ()))
480           return aPhase;
481     return null;
482   }
483 
484   public void addPattern (@Nonnull final PSPattern aPattern)
485   {
486     ValueEnforcer.notNull (aPattern, "Pattern");
487     m_aPatterns.add (aPattern);
488   }
489 
490   public boolean hasPatterns ()
491   {
492     return m_aPatterns.isNotEmpty ();
493   }
494 
495   public boolean hasNoPatterns ()
496   {
497     return m_aPatterns.isEmpty ();
498   }
499 
500   @Nonnull
501   @ReturnsMutableCopy
502   public ICommonsList <PSPattern> getAllPatterns ()
503   {
504     return m_aPatterns.getClone ();
505   }
506 
507   @Nonnegative
508   public int getPatternCount ()
509   {
510     return m_aPatterns.size ();
511   }
512 
513   @Nullable
514   public PSPattern getPatternOfID (@Nullable final String sID)
515   {
516     if (StringHelper.hasText (sID))
517       for (final PSPattern aPattern : m_aPatterns)
518         if (sID.equals (aPattern.getID ()))
519           return aPattern;
520     return null;
521   }
522 
523   public void addEndP (@Nonnull final PSP aP)
524   {
525     ValueEnforcer.notNull (aP, "P");
526     m_aEndPs.add (aP);
527   }
528 
529   @Nonnull
530   @ReturnsMutableCopy
531   public ICommonsList <PSP> getAllEndPs ()
532   {
533     return m_aEndPs.getClone ();
534   }
535 
536   public void setDiagnostics (@Nullable final PSDiagnostics aDiagnostics)
537   {
538     m_aDiagnostics = aDiagnostics;
539   }
540 
541   public boolean hasDiagnostics ()
542   {
543     return m_aDiagnostics != null;
544   }
545 
546   @Nullable
547   public PSDiagnostics getDiagnostics ()
548   {
549     return m_aDiagnostics;
550   }
551 
552   @Nonnull
553   public IMicroElement getAsMicroElement ()
554   {
555     final IMicroElement ret = new MicroElement (CSchematron.NAMESPACE_SCHEMATRON, CSchematronXML.ELEMENT_SCHEMA);
556     ret.setAttribute (CSchematronXML.ATTR_ID, m_sID);
557     if (m_aRich != null)
558       m_aRich.fillMicroElement (ret);
559     ret.setAttribute (CSchematronXML.ATTR_SCHEMA_VERSION, m_sSchemaVersion);
560     ret.setAttribute (CSchematronXML.ATTR_DEFAULT_PHASE, m_sDefaultPhase);
561     ret.setAttribute (CSchematronXML.ATTR_QUERY_BINDING, m_sQueryBinding);
562     if (m_aForeignElements != null)
563       for (final IMicroElement aForeignElement : m_aForeignElements)
564         ret.appendChild (aForeignElement.getClone ());
565     for (final PSInclude aInclude : m_aIncludes)
566       ret.appendChild (aInclude.getAsMicroElement ());
567     if (m_aTitle != null)
568       ret.appendChild (m_aTitle.getAsMicroElement ());
569     for (final PSNS aNS : m_aNSs)
570       ret.appendChild (aNS.getAsMicroElement ());
571     for (final PSP aP : m_aStartPs)
572       ret.appendChild (aP.getAsMicroElement ());
573     for (final PSLet aLet : m_aLets)
574       ret.appendChild (aLet.getAsMicroElement ());
575     for (final PSPhase aPhase : m_aPhases)
576       ret.appendChild (aPhase.getAsMicroElement ());
577     for (final PSPattern aPattern : m_aPatterns)
578       ret.appendChild (aPattern.getAsMicroElement ());
579     for (final PSP aP : m_aEndPs)
580       ret.appendChild (aP.getAsMicroElement ());
581     if (m_aDiagnostics != null)
582       ret.appendChild (m_aDiagnostics.getAsMicroElement ());
583     if (m_aForeignAttrs != null)
584       for (final Map.Entry <String, String> aEntry : m_aForeignAttrs.entrySet ())
585         ret.setAttribute (aEntry.getKey (), aEntry.getValue ());
586     return ret;
587   }
588 
589   @Override
590   public String toString ()
591   {
592     return new ToStringGenerator (this).appendIfNotNull ("resource", m_aResource)
593                                        .appendIfNotNull ("id", m_sID)
594                                        .appendIfNotNull ("rich", m_aRich)
595                                        .appendIfNotNull ("schemaVersion", m_sSchemaVersion)
596                                        .appendIfNotNull ("defaultPhase", m_sDefaultPhase)
597                                        .appendIfNotNull ("queryBinding", m_sQueryBinding)
598                                        .appendIfNotNull ("title", m_aTitle)
599                                        .appendIf ("includes", m_aIncludes, CollectionHelper::isNotEmpty)
600                                        .appendIf ("nss", m_aNSs, CollectionHelper::isNotEmpty)
601                                        .appendIf ("startps", m_aStartPs, CollectionHelper::isNotEmpty)
602                                        .appendIf ("lets", m_aLets, CollectionHelper::isNotEmpty)
603                                        .appendIf ("phases", m_aPhases, CollectionHelper::isNotEmpty)
604                                        .appendIf ("patterns", m_aPatterns, CollectionHelper::isNotEmpty)
605                                        .appendIf ("endps", m_aEndPs, CollectionHelper::isNotEmpty)
606                                        .appendIfNotNull ("diagnostics", m_aDiagnostics)
607                                        .appendIf ("foreignAttrs", m_aForeignAttrs, CollectionHelper::isNotEmpty)
608                                        .appendIf ("foreignElements", m_aForeignElements, CollectionHelper::isNotEmpty)
609                                        .getToString ();
610   }
611 }