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.microdom.IMicroElement;
33  import com.helger.commons.microdom.MicroElement;
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  
40  /**
41   * A single Schematron pattern-element.<br>
42   * A structure, simple or complex. A set of rules giving constraints that are in
43   * some way related. The id attribute provides a unique name for the pattern and
44   * is required for abstract patterns.<br>
45   * The title and p elements allow rich documentation.<br>
46   * The icon, see and fpi attributes allow rich interfaces and documentation.<br>
47   * When a pattern element has the attribute abstract with a value true, then the
48   * pattern defines an abstract pattern. An abstract pattern shall not have a
49   * is-a attribute and shall have an id attribute.<br>
50   * Abstract patterns allow a common definition mechanism for structures which
51   * use different names and paths, but which are at heart the same. For example,
52   * there are different table markup languages, but they all can be in large part
53   * represented as an abstract pattern where a table contains rows and rows
54   * contain entries, as defined in the following example using the default query
55   * language binding:<br>
56   *
57   * <pre>
58   *     &lt;sch:pattern abstract="true" id="table"&gt;
59   *         &lt;sch:rule context="$table"&gt;
60   *             &lt;sch:assert test="$row"&gt;
61   *             The element &lt;sch:name/&gt; is a table. Tables contain rows.
62   *             &lt;/sch:assert&gt;
63   *         &lt;/sch:rule&gt;
64   *         &lt;sch:rule context="$row"&gt;
65   *             &lt;sch:assert test="$entry"&gt;
66   *             The element &lt;sch:name/&gt; is a table row. Rows contain entries.
67   *             &lt;/sch:assert&gt;
68   *         &lt;/sch:rule&gt;
69   *     &lt;/sch:pattern&gt;
70   * </pre>
71   *
72   * When a pattern element has the attribute is-a with a value specifying the
73   * name of an abstract pattern, then the pattern is an instance of an abstract
74   * pattern. Such a pattern shall not contain any rule elements, but shall have
75   * param elements for all parameters used in the abstract pattern.<br>
76   * The following example uses the abstract pattern for tables given above to
77   * create three patterns for tables with different names or structures.<br>
78   *
79   * <pre>
80   *     &lt;sch:pattern is-a="table" id="HTML_Table"&gt;
81   *         &lt;sch:param name="table" value="table"/&gt;
82   *         &lt;sch:param name="row"   value="tr"/&gt;
83   *         &lt;sch:param name="entry" value="td|th"/&gt;
84   *     &lt;/sch:pattern&gt;
85   *     &lt;sch:pattern is-a="table" id="CALS_Table"&gt;
86   *         &lt;sch:param name="table" value="table"/&gt;
87   *         &lt;sch:param name="row"   value=".//row"/&gt;
88   *         &lt;sch:param name="entry" value="cell"/&gt;
89   *     &lt;/sch:pattern&gt;
90   *     &lt;sch:pattern is-a="table" id="calendar"&gt;
91   *         &lt;sch:param name="table" value="calendar/year"/&gt;
92   *         &lt;sch:param name="row"   value="week"/&gt;
93   *         &lt;sch:param name="entry" value="day"/&gt;
94   *     &lt;/sch:pattern&gt;
95   * </pre>
96   *
97   * When creating an instance of an abstract pattern, the parameter values
98   * supplied by the param element replace the parameter references used in the
99   * abstract patterns. The examples above use the default query language binding
100  * in which the character $ is used as the delimiter for parameter references.
101  * <br>
102  * Thus, given the abstract patterns defined earlier in this clause, the
103  * patterns defined above are equivalent to the following, with the id elements
104  * shown expanded:<br>
105  *
106  * <pre>
107  *     &lt;sch:pattern id="HTML_table"&gt;
108  *         &lt;sch:rule context="table"&gt;
109  *             &lt;sch:assert test="tr"&gt;
110  *             The element table is a table. Tables containing rows.
111  *             &lt;/sch:assert&gt;
112  *         &lt;/sch:rule&gt;
113  *         &lt;sch:rule context="tr"&gt;
114  *             &lt;sch:assert test="td|th"&gt;
115  *             The element tr is a table row. Rows contain entries.
116  *             &lt;/sch:assert&gt;
117  *         &lt;/sch:rule&gt;
118  *     &lt;/sch:pattern&gt;
119  *     &lt;sch:pattern id="CALS_table"&gt;
120  *         &lt;sch:rule context="table"&gt;
121  *             &lt;sch:assert test=".//row"&gt;
122  *             The element table is a table. Tables containing rows.
123  *             &lt;/sch:assert&gt;
124  *         &lt;/sch:rule&gt;
125  *         &lt;sch:rule context=".//row"&gt;
126  *             &lt;sch:assert test="cell"&gt;
127  *             The element row is a table row. Rows contain entries.
128  *             &lt;/sch:assert&gt;
129  *         &lt;/sch:rule&gt;
130  *     &lt;/sch:pattern&gt;
131  *     &lt;sch:pattern id="calendar"&gt;
132  *         &lt;sch:rule context="calendar/year"&gt;
133  *             &lt;sch:assert test="week"&gt;
134  *             The element year is a table. Tables containing rows.
135  *             &lt;/sch:assert&gt;
136  *         &lt;/sch:rule&gt;
137  *         &lt;sch:rule context="week"&gt;
138  *             &lt;sch:assert test="day"&gt;
139  *             The element week is a table row. Rows contain entries.
140  *             &lt;/sch:assert&gt;
141  *         &lt;/sch:rule&gt;
142  *     &lt;/sch:pattern&gt;
143  * </pre>
144  *
145  * @author Philip Helger
146  */
147 @NotThreadSafe
148 public class PSPattern implements IPSElement, IPSHasID, IPSHasForeignElements, IPSHasIncludes, IPSHasLets, IPSHasRichGroup
149 {
150   private boolean m_bAbstract = false;
151   private String m_sID;
152   private String m_sIsA;
153   private PSRichGroup m_aRich;
154   private final List <PSInclude> m_aIncludes = new ArrayList <PSInclude> ();
155   private PSTitle m_aTitle;
156   private final List <IPSElement> m_aContent = new ArrayList <IPSElement> ();
157   private Map <String, String> m_aForeignAttrs;
158   private List <IMicroElement> m_aForeignElements;
159 
160   public PSPattern ()
161   {}
162 
163   public boolean isValid (@Nonnull final IPSErrorHandler aErrorHandler)
164   {
165     // If abstract, an ID must be present
166     if (m_bAbstract && StringHelper.hasNoText (m_sID))
167     {
168       aErrorHandler.error (this, "abstract <pattern> does not have an 'id'");
169       return false;
170     }
171     // is-a may not be present for abstract rules
172     if (m_bAbstract && StringHelper.hasText (m_sIsA))
173     {
174       aErrorHandler.error (this, "abstract <pattern> may not have an 'is-a'");
175       return false;
176     }
177     if (StringHelper.hasNoText (m_sIsA))
178     {
179       // param only if is-a is set
180       for (final IPSElement aContent : m_aContent)
181         if (aContent instanceof PSParam)
182         {
183           aErrorHandler.error (this, "<pattern> without 'is-a' may not contain <param>s");
184           return false;
185         }
186     }
187     else
188     {
189       // rule and let only if is-a is not set
190       for (final IPSElement aContent : m_aContent)
191       {
192         if (aContent instanceof PSRule)
193         {
194           aErrorHandler.error (this, "<pattern> with 'is-a' may not contain <rule>s");
195           return false;
196         }
197         if (aContent instanceof PSLet)
198         {
199           aErrorHandler.error (this, "<pattern> with 'is-a' may not contain <let>s");
200           return false;
201         }
202       }
203     }
204 
205     for (final PSInclude aInclude : m_aIncludes)
206       if (!aInclude.isValid (aErrorHandler))
207         return false;
208     if (m_aTitle != null && !m_aTitle.isValid (aErrorHandler))
209       return false;
210     for (final IPSElement aContent : m_aContent)
211       if (!aContent.isValid (aErrorHandler))
212         return false;
213     return true;
214   }
215 
216   public void validateCompletely (@Nonnull final IPSErrorHandler aErrorHandler)
217   {
218     // If abstract, an ID must be present
219     if (m_bAbstract && StringHelper.hasNoText (m_sID))
220       aErrorHandler.error (this, "abstract <pattern> does not have an 'id'");
221     // is-a may not be present for abstract rules
222     if (m_bAbstract && StringHelper.hasText (m_sIsA))
223       aErrorHandler.error (this, "abstract <pattern> may not have an 'is-a'");
224     if (StringHelper.hasNoText (m_sIsA))
225     {
226       // param only if is-a is set
227       for (final IPSElement aContent : m_aContent)
228         if (aContent instanceof PSParam)
229           aErrorHandler.error (this, "<pattern> without 'is-a' may not contain <param>s");
230     }
231     else
232     {
233       // rule and let only if is-a is not set
234       for (final IPSElement aContent : m_aContent)
235       {
236         if (aContent instanceof PSRule)
237           aErrorHandler.error (this, "<pattern> with 'is-a' may not contain <rule>s");
238         if (aContent instanceof PSLet)
239           aErrorHandler.error (this, "<pattern> with 'is-a' may not contain <let>s");
240       }
241     }
242 
243     for (final PSInclude aInclude : m_aIncludes)
244       aInclude.validateCompletely (aErrorHandler);
245     if (m_aTitle != null)
246       m_aTitle.validateCompletely (aErrorHandler);
247     for (final IPSElement aContent : m_aContent)
248       aContent.validateCompletely (aErrorHandler);
249   }
250 
251   public boolean isMinimal ()
252   {
253     if (m_bAbstract)
254       return false;
255     if (StringHelper.hasText (m_sIsA))
256       return false;
257     for (final PSInclude aInclude : m_aIncludes)
258       if (!aInclude.isMinimal ())
259         return false;
260     if (m_aTitle != null && !m_aTitle.isMinimal ())
261       return false;
262     for (final IPSElement aContent : m_aContent)
263       if (!aContent.isMinimal ())
264         return false;
265     return true;
266   }
267 
268   public void addForeignElement (@Nonnull final IMicroElement aForeignElement)
269   {
270     ValueEnforcer.notNull (aForeignElement, "ForeignElement");
271     if (aForeignElement.hasParent ())
272       throw new IllegalArgumentException ("ForeignElement already has a parent!");
273     if (m_aForeignElements == null)
274       m_aForeignElements = new ArrayList <IMicroElement> ();
275     m_aForeignElements.add (aForeignElement);
276   }
277 
278   public void addForeignElements (@Nonnull final List <IMicroElement> aForeignElements)
279   {
280     ValueEnforcer.notNull (aForeignElements, "ForeignElements");
281     for (final IMicroElement aForeignElement : aForeignElements)
282       addForeignElement (aForeignElement);
283   }
284 
285   public boolean hasForeignElements ()
286   {
287     return m_aForeignElements != null && !m_aForeignElements.isEmpty ();
288   }
289 
290   @Nonnull
291   @ReturnsMutableCopy
292   public List <IMicroElement> getAllForeignElements ()
293   {
294     return CollectionHelper.newList (m_aForeignElements);
295   }
296 
297   public void addForeignAttribute (@Nonnull final String sAttrName, @Nonnull final String sAttrValue)
298   {
299     ValueEnforcer.notNull (sAttrName, "AttrName");
300     ValueEnforcer.notNull (sAttrValue, "AttrValue");
301     if (m_aForeignAttrs == null)
302       m_aForeignAttrs = new LinkedHashMap <String, String> ();
303     m_aForeignAttrs.put (sAttrName, sAttrValue);
304   }
305 
306   public void addForeignAttributes (@Nonnull final Map <String, String> aForeignAttrs)
307   {
308     ValueEnforcer.notNull (aForeignAttrs, "ForeignAttrs");
309     for (final Map.Entry <String, String> aEntry : aForeignAttrs.entrySet ())
310       addForeignAttribute (aEntry.getKey (), aEntry.getValue ());
311   }
312 
313   public boolean hasForeignAttributes ()
314   {
315     return m_aForeignAttrs != null && !m_aForeignAttrs.isEmpty ();
316   }
317 
318   @Nonnull
319   @ReturnsMutableCopy
320   public Map <String, String> getAllForeignAttributes ()
321   {
322     return CollectionHelper.newOrderedMap (m_aForeignAttrs);
323   }
324 
325   public void setAbstract (final boolean bAbstract)
326   {
327     m_bAbstract = bAbstract;
328   }
329 
330   public boolean isAbstract ()
331   {
332     return m_bAbstract;
333   }
334 
335   public void setID (@Nullable final String sID)
336   {
337     m_sID = sID;
338   }
339 
340   public boolean hasID ()
341   {
342     return m_sID != null;
343   }
344 
345   @Nullable
346   public String getID ()
347   {
348     return m_sID;
349   }
350 
351   public void setIsA (@Nullable final String sIsA)
352   {
353     m_sIsA = sIsA;
354   }
355 
356   @Nullable
357   public String getIsA ()
358   {
359     return m_sIsA;
360   }
361 
362   public void setRich (@Nullable final PSRichGroup aRich)
363   {
364     m_aRich = aRich;
365   }
366 
367   public boolean hasRich ()
368   {
369     return m_aRich != null;
370   }
371 
372   @Nullable
373   public PSRichGroup getRich ()
374   {
375     return m_aRich;
376   }
377 
378   @Nullable
379   public PSRichGroup getRichClone ()
380   {
381     return m_aRich == null ? null : m_aRich.getClone ();
382   }
383 
384   public void addInclude (@Nonnull final PSInclude aInclude)
385   {
386     ValueEnforcer.notNull (aInclude, "Include");
387     m_aIncludes.add (aInclude);
388   }
389 
390   public boolean hasAnyInclude ()
391   {
392     return !m_aIncludes.isEmpty ();
393   }
394 
395   @Nonnull
396   @ReturnsMutableCopy
397   public List <PSInclude> getAllIncludes ()
398   {
399     return CollectionHelper.newList (m_aIncludes);
400   }
401 
402   public void setTitle (@Nullable final PSTitle aTitle)
403   {
404     m_aTitle = aTitle;
405   }
406 
407   @Nullable
408   public PSTitle getTitle ()
409   {
410     return m_aTitle;
411   }
412 
413   public boolean hasTitle ()
414   {
415     return m_aTitle != null;
416   }
417 
418   public void addRule (@Nonnull final PSRule aRule)
419   {
420     ValueEnforcer.notNull (aRule, "Rule");
421     m_aContent.add (aRule);
422   }
423 
424   @Nullable
425   public PSRule getRuleOfID (@Nullable final String sID)
426   {
427     if (StringHelper.hasText (sID))
428       for (final IPSElement aElement : m_aContent)
429         if (aElement instanceof PSRule)
430         {
431           final PSRule aRule = (PSRule) aElement;
432           if (sID.equals (aRule.getID ()))
433             return aRule;
434         }
435     return null;
436   }
437 
438   @Nonnull
439   @ReturnsMutableCopy
440   public List <PSRule> getAllRules ()
441   {
442     final List <PSRule> ret = new ArrayList <PSRule> ();
443     for (final IPSElement aElement : m_aContent)
444       if (aElement instanceof PSRule)
445         ret.add ((PSRule) aElement);
446     return ret;
447   }
448 
449   @Nonnegative
450   public int getRuleCount ()
451   {
452     int ret = 0;
453     for (final IPSElement aElement : m_aContent)
454       if (aElement instanceof PSRule)
455         ret++;
456     return ret;
457   }
458 
459   public void addParam (@Nonnull final PSParam aParam)
460   {
461     ValueEnforcer.notNull (aParam, "Param");
462     m_aContent.add (aParam);
463   }
464 
465   @Nonnull
466   @ReturnsMutableCopy
467   public List <PSParam> getAllParams ()
468   {
469     final List <PSParam> ret = new ArrayList <PSParam> ();
470     for (final IPSElement aElement : m_aContent)
471       if (aElement instanceof PSParam)
472         ret.add ((PSParam) aElement);
473     return ret;
474   }
475 
476   public boolean hasAnyParam ()
477   {
478     for (final IPSElement aElement : m_aContent)
479       if (aElement instanceof PSParam)
480         return true;
481     return false;
482   }
483 
484   public void addP (@Nonnull final PSP aP)
485   {
486     ValueEnforcer.notNull (aP, "P");
487     m_aContent.add (aP);
488   }
489 
490   @Nonnull
491   @ReturnsMutableCopy
492   public List <PSP> getAllPs ()
493   {
494     final List <PSP> ret = new ArrayList <PSP> ();
495     for (final IPSElement aElement : m_aContent)
496       if (aElement instanceof PSP)
497         ret.add ((PSP) aElement);
498     return ret;
499   }
500 
501   public void addLet (@Nonnull final PSLet aLet)
502   {
503     ValueEnforcer.notNull (aLet, "Let");
504     m_aContent.add (aLet);
505   }
506 
507   public boolean hasAnyLet ()
508   {
509     for (final IPSElement aElement : m_aContent)
510       if (aElement instanceof PSLet)
511         return true;
512     return false;
513   }
514 
515   @Nonnull
516   @ReturnsMutableCopy
517   public List <PSLet> getAllLets ()
518   {
519     final List <PSLet> ret = new ArrayList <PSLet> ();
520     for (final IPSElement aElement : m_aContent)
521       if (aElement instanceof PSLet)
522         ret.add ((PSLet) aElement);
523     return ret;
524   }
525 
526   @Nonnull
527   @ReturnsMutableCopy
528   public Map <String, String> getAllLetsAsMap ()
529   {
530     final Map <String, String> ret = new LinkedHashMap <String, String> ();
531     for (final IPSElement aElement : m_aContent)
532       if (aElement instanceof PSLet)
533       {
534         final PSLet aLet = (PSLet) aElement;
535         ret.put (aLet.getName (), aLet.getValue ());
536       }
537     return ret;
538   }
539 
540   /**
541    * @return A list consisting of {@link PSP}, {@link PSLet}, {@link PSRule} and
542    *         {@link PSParam} parameters
543    */
544   @Nonnull
545   @ReturnsMutableCopy
546   public List <IPSElement> getAllContentElements ()
547   {
548     return CollectionHelper.newList (m_aContent);
549   }
550 
551   @Nonnull
552   public IMicroElement getAsMicroElement ()
553   {
554     final IMicroElement ret = new MicroElement (CSchematron.NAMESPACE_SCHEMATRON, CSchematronXML.ELEMENT_PATTERN);
555     if (m_bAbstract)
556       ret.setAttribute (CSchematronXML.ATTR_ABSTRACT, "true");
557     ret.setAttribute (CSchematronXML.ATTR_ID, m_sID);
558     ret.setAttribute (CSchematronXML.ATTR_IS_A, m_sIsA);
559     if (m_aRich != null)
560       m_aRich.fillMicroElement (ret);
561     if (m_aForeignElements != null)
562       for (final IMicroElement aForeignElement : m_aForeignElements)
563         ret.appendChild (aForeignElement.getClone ());
564     for (final PSInclude aInclude : m_aIncludes)
565       ret.appendChild (aInclude.getAsMicroElement ());
566     if (m_aTitle != null)
567       ret.appendChild (m_aTitle.getAsMicroElement ());
568     for (final IPSElement aContent : m_aContent)
569       ret.appendChild (aContent.getAsMicroElement ());
570     if (m_aForeignAttrs != null)
571       for (final Map.Entry <String, String> aEntry : m_aForeignAttrs.entrySet ())
572         ret.setAttribute (aEntry.getKey (), aEntry.getValue ());
573     return ret;
574   }
575 
576   @Override
577   public String toString ()
578   {
579     return new ToStringGenerator (this).append ("abstract", m_bAbstract)
580                                        .appendIfNotNull ("id", m_sID)
581                                        .appendIfNotNull ("is-a", m_sIsA)
582                                        .appendIfNotNull ("rich", m_aRich)
583                                        .appendIfNotEmpty ("includes", m_aIncludes)
584                                        .appendIfNotNull ("title", m_aTitle)
585                                        .appendIfNotEmpty ("content", m_aContent)
586                                        .appendIfNotEmpty ("foreignAttrs", m_aForeignAttrs)
587                                        .appendIfNotEmpty ("foreignElements", m_aForeignElements)
588                                        .toString ();
589   }
590 }