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