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