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