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 rule-element.<br>
42   * A list of assertions tested within the context specified by the required
43   * context attribute. The context attribute specifies the rule context
44   * expression.<br>
45   * NOTE: It is not an error if a rule never fires in a document. In order to
46   * test that a document always has some context, a new pattern should be created
47   * from the context of the document, with an assertion requiring the element or
48   * attribute.<br>
49   * The icon, see and fpi attributes allow rich interfaces and documentation.<br>
50   * The flag attribute allows more detailed outcomes.<br>
51   * The role and subject attributes allow explicit identification of some part of
52   * a pattern as part of the validation outcome.<br>
53   * When the rule element has the attribute abstract with a value true, then the
54   * rule is an abstract rule. An abstract rule shall not have a context
55   * attribute. An abstract rule is a list of assertions that will be invoked by
56   * other rules belonging to the same pattern using the extends element. Abstract
57   * rules provide a mechanism for reducing schema size.
58   *
59   * @author Philip Helger
60   */
61  @NotThreadSafe
62  public class PSRule implements IPSElement, IPSHasID, IPSHasFlag, IPSHasForeignElements, IPSHasIncludes, IPSHasLets, IPSHasRichGroup, IPSHasLinkableGroup
63  {
64    public static final boolean DEFAULT_ABSTRACT = false;
65  
66    private String m_sFlag;
67    private PSRichGroup m_aRich;
68    private PSLinkableGroup m_aLinkable;
69    private boolean m_bAbstract = DEFAULT_ABSTRACT;
70    private String m_sContext;
71    private String m_sID;
72    private final List <PSInclude> m_aIncludes = new ArrayList <PSInclude> ();
73    private final List <PSLet> m_aLets = new ArrayList <PSLet> ();
74    private final List <IPSElement> m_aContent = new ArrayList <IPSElement> ();
75    private Map <String, String> m_aForeignAttrs;
76    private List <IMicroElement> m_aForeignElements;
77  
78    public PSRule ()
79    {}
80  
81    public boolean isValid (@Nonnull final IPSErrorHandler aErrorHandler)
82    {
83      // abstract rules need an ID
84      if (m_bAbstract && StringHelper.hasNoText (m_sID))
85      {
86        aErrorHandler.error (this, "abstract <rule> has no 'id'");
87        return false;
88      }
89      // abstract rules may not have a context
90      if (m_bAbstract && StringHelper.hasText (m_sContext))
91      {
92        aErrorHandler.error (this, "abstract <rule> may not have a 'context'");
93        return false;
94      }
95      // Non-abstract rules need a context
96      if (!m_bAbstract && StringHelper.hasNoText (m_sContext))
97      {
98        aErrorHandler.error (this, "<rule> must have a 'context'");
99        return false;
100     }
101     // At least one assert, report or extends must be present
102     if (m_aContent.isEmpty ())
103     {
104       aErrorHandler.error (this, "<rule> has no content");
105       return false;
106     }
107     for (final PSInclude aInclude : m_aIncludes)
108       if (!aInclude.isValid (aErrorHandler))
109         return false;
110     for (final PSLet aLet : m_aLets)
111       if (!aLet.isValid (aErrorHandler))
112         return false;
113     for (final IPSElement aContent : m_aContent)
114       if (!aContent.isValid (aErrorHandler))
115         return false;
116     return true;
117   }
118 
119   public void validateCompletely (@Nonnull final IPSErrorHandler aErrorHandler)
120   {
121     // abstract rules need an ID
122     if (m_bAbstract && StringHelper.hasNoText (m_sID))
123       aErrorHandler.error (this, "abstract <rule> has no 'id'");
124     // abstract rules may not have a context
125     if (m_bAbstract && StringHelper.hasText (m_sContext))
126       aErrorHandler.error (this, "abstract <rule> may not have a 'context'");
127     // Non-abstract rules need a context
128     if (!m_bAbstract && StringHelper.hasNoText (m_sContext))
129       aErrorHandler.error (this, "<rule> must have a 'context'");
130     // At least one assert, report or extends must be present
131     if (m_aContent.isEmpty ())
132       aErrorHandler.error (this, "<rule> has no content");
133     for (final PSInclude aInclude : m_aIncludes)
134       aInclude.validateCompletely (aErrorHandler);
135     for (final PSLet aLet : m_aLets)
136       aLet.validateCompletely (aErrorHandler);
137     for (final IPSElement aContent : m_aContent)
138       aContent.validateCompletely (aErrorHandler);
139   }
140 
141   public boolean isMinimal ()
142   {
143     for (final PSInclude aInclude : m_aIncludes)
144       if (!aInclude.isMinimal ())
145         return false;
146     for (final PSLet aLet : m_aLets)
147       if (!aLet.isMinimal ())
148         return false;
149     for (final IPSElement aContent : m_aContent)
150       if (!aContent.isMinimal ())
151         return false;
152     return true;
153   }
154 
155   public void addForeignElement (@Nonnull final IMicroElement aForeignElement)
156   {
157     ValueEnforcer.notNull (aForeignElement, "ForeignElement");
158     if (aForeignElement.hasParent ())
159       throw new IllegalArgumentException ("ForeignElement already has a parent!");
160     if (m_aForeignElements == null)
161       m_aForeignElements = new ArrayList <IMicroElement> ();
162     m_aForeignElements.add (aForeignElement);
163   }
164 
165   public void addForeignElements (@Nonnull final List <IMicroElement> aForeignElements)
166   {
167     ValueEnforcer.notNull (aForeignElements, "ForeignElements");
168     for (final IMicroElement aForeignElement : aForeignElements)
169       addForeignElement (aForeignElement);
170   }
171 
172   public boolean hasForeignElements ()
173   {
174     return m_aForeignElements != null && !m_aForeignElements.isEmpty ();
175   }
176 
177   @Nonnull
178   @ReturnsMutableCopy
179   public List <IMicroElement> getAllForeignElements ()
180   {
181     return CollectionHelper.newList (m_aForeignElements);
182   }
183 
184   public void addForeignAttribute (@Nonnull final String sAttrName, @Nonnull final String sAttrValue)
185   {
186     ValueEnforcer.notNull (sAttrName, "AttrName");
187     ValueEnforcer.notNull (sAttrValue, "AttrValue");
188     if (m_aForeignAttrs == null)
189       m_aForeignAttrs = new LinkedHashMap <String, String> ();
190     m_aForeignAttrs.put (sAttrName, sAttrValue);
191   }
192 
193   public void addForeignAttributes (@Nonnull final Map <String, String> aForeignAttrs)
194   {
195     ValueEnforcer.notNull (aForeignAttrs, "ForeignAttrs");
196     for (final Map.Entry <String, String> aEntry : aForeignAttrs.entrySet ())
197       addForeignAttribute (aEntry.getKey (), aEntry.getValue ());
198   }
199 
200   public boolean hasForeignAttributes ()
201   {
202     return m_aForeignAttrs != null && !m_aForeignAttrs.isEmpty ();
203   }
204 
205   @Nonnull
206   @ReturnsMutableCopy
207   public Map <String, String> getAllForeignAttributes ()
208   {
209     return CollectionHelper.newOrderedMap (m_aForeignAttrs);
210   }
211 
212   public void setFlag (@Nullable final String sFlag)
213   {
214     m_sFlag = sFlag;
215   }
216 
217   @Nullable
218   public String getFlag ()
219   {
220     return m_sFlag;
221   }
222 
223   public void setRich (@Nullable final PSRichGroup aRich)
224   {
225     m_aRich = aRich;
226   }
227 
228   public boolean hasRich ()
229   {
230     return m_aRich != null;
231   }
232 
233   @Nullable
234   public PSRichGroup getRich ()
235   {
236     return m_aRich;
237   }
238 
239   @Nullable
240   public PSRichGroup getRichClone ()
241   {
242     return m_aRich == null ? null : m_aRich.getClone ();
243   }
244 
245   public void setLinkable (@Nullable final PSLinkableGroup aLinkable)
246   {
247     m_aLinkable = aLinkable;
248   }
249 
250   public boolean hasLinkable ()
251   {
252     return m_aLinkable != null;
253   }
254 
255   @Nullable
256   public PSLinkableGroup getLinkable ()
257   {
258     return m_aLinkable;
259   }
260 
261   @Nullable
262   public PSLinkableGroup getLinkableClone ()
263   {
264     return m_aLinkable == null ? null : m_aLinkable.getClone ();
265   }
266 
267   /**
268    * @param bAbstract
269    *        The abstract state of this rule.
270    */
271   public void setAbstract (final boolean bAbstract)
272   {
273     m_bAbstract = bAbstract;
274   }
275 
276   /**
277    * @return <code>true</code> if this rule is abstract, <code>false</code>
278    *         otherwise. Default is {@value #DEFAULT_ABSTRACT}.
279    */
280   public boolean isAbstract ()
281   {
282     return m_bAbstract;
283   }
284 
285   public void setContext (@Nullable final String sContext)
286   {
287     m_sContext = sContext;
288   }
289 
290   @Nullable
291   public String getContext ()
292   {
293     return m_sContext;
294   }
295 
296   public void setID (@Nullable final String sID)
297   {
298     m_sID = sID;
299   }
300 
301   public boolean hasID ()
302   {
303     return m_sID != null;
304   }
305 
306   @Nullable
307   public String getID ()
308   {
309     return m_sID;
310   }
311 
312   public void addInclude (@Nonnull final PSInclude aInclude)
313   {
314     ValueEnforcer.notNull (aInclude, "Include");
315     m_aIncludes.add (aInclude);
316   }
317 
318   public boolean hasAnyInclude ()
319   {
320     return !m_aIncludes.isEmpty ();
321   }
322 
323   @Nonnull
324   @ReturnsMutableCopy
325   public List <PSInclude> getAllIncludes ()
326   {
327     return CollectionHelper.newList (m_aIncludes);
328   }
329 
330   public void addLet (@Nonnull final PSLet aLet)
331   {
332     ValueEnforcer.notNull (aLet, "Let");
333     m_aLets.add (aLet);
334   }
335 
336   public boolean hasAnyLet ()
337   {
338     return !m_aLets.isEmpty ();
339   }
340 
341   @Nonnull
342   @ReturnsMutableCopy
343   public List <PSLet> getAllLets ()
344   {
345     return CollectionHelper.newList (m_aLets);
346   }
347 
348   @Nonnull
349   @ReturnsMutableCopy
350   public Map <String, String> getAllLetsAsMap ()
351   {
352     final Map <String, String> ret = new LinkedHashMap <String, String> ();
353     for (final PSLet aLet : m_aLets)
354       ret.put (aLet.getName (), aLet.getValue ());
355     return ret;
356   }
357 
358   public void addAssertReport (@Nonnull final PSAssertReport aAssertReport)
359   {
360     ValueEnforcer.notNull (aAssertReport, "AssertReport");
361     m_aContent.add (aAssertReport);
362   }
363 
364   @Nonnull
365   @ReturnsMutableCopy
366   public List <PSAssertReport> getAllAssertReports ()
367   {
368     final List <PSAssertReport> ret = new ArrayList <PSAssertReport> ();
369     for (final IPSElement aElement : m_aContent)
370       if (aElement instanceof PSAssertReport)
371         ret.add ((PSAssertReport) aElement);
372     return ret;
373   }
374 
375   public void addExtends (@Nonnull final PSExtends aExtends)
376   {
377     ValueEnforcer.notNull (aExtends, "Extends");
378     m_aContent.add (aExtends);
379   }
380 
381   @Nonnull
382   @ReturnsMutableCopy
383   public List <PSExtends> getAllExtends ()
384   {
385     final List <PSExtends> ret = new ArrayList <PSExtends> ();
386     for (final IPSElement aElement : m_aContent)
387       if (aElement instanceof PSExtends)
388         ret.add ((PSExtends) aElement);
389     return ret;
390   }
391 
392   @Nonnegative
393   public int getExtendsCount ()
394   {
395     int ret = 0;
396     for (final IPSElement aElement : m_aContent)
397       if (aElement instanceof PSExtends)
398         ++ret;
399     return ret;
400   }
401 
402   public boolean hasAnyExtends ()
403   {
404     for (final IPSElement aElement : m_aContent)
405       if (aElement instanceof PSExtends)
406         return true;
407     return false;
408   }
409 
410   /**
411    * @return A list consisting of {@link PSAssertReport} and {@link PSExtends}
412    *         parameters
413    */
414   @Nonnull
415   @ReturnsMutableCopy
416   public List <IPSElement> getAllContentElements ()
417   {
418     return CollectionHelper.newList (m_aContent);
419   }
420 
421   @Nonnull
422   public IMicroElement getAsMicroElement ()
423   {
424     final IMicroElement ret = new MicroElement (CSchematron.NAMESPACE_SCHEMATRON, CSchematronXML.ELEMENT_RULE);
425     ret.setAttribute (CSchematronXML.ATTR_FLAG, m_sFlag);
426     if (m_bAbstract)
427       ret.setAttribute (CSchematronXML.ATTR_ABSTRACT, "true");
428     ret.setAttribute (CSchematronXML.ATTR_CONTEXT, m_sContext);
429     ret.setAttribute (CSchematronXML.ATTR_ID, m_sID);
430     if (m_aRich != null)
431       m_aRich.fillMicroElement (ret);
432     if (m_aLinkable != null)
433       m_aLinkable.fillMicroElement (ret);
434     if (m_aForeignElements != null)
435       for (final IMicroElement aForeignElement : m_aForeignElements)
436         ret.appendChild (aForeignElement.getClone ());
437     for (final PSInclude aInclude : m_aIncludes)
438       ret.appendChild (aInclude.getAsMicroElement ());
439     for (final PSLet aLet : m_aLets)
440       ret.appendChild (aLet.getAsMicroElement ());
441     for (final IPSElement aContent : m_aContent)
442       ret.appendChild (aContent.getAsMicroElement ());
443     if (m_aForeignAttrs != null)
444       for (final Map.Entry <String, String> aEntry : m_aForeignAttrs.entrySet ())
445         ret.setAttribute (aEntry.getKey (), aEntry.getValue ());
446     return ret;
447   }
448 
449   @Override
450   public String toString ()
451   {
452     return new ToStringGenerator (this).appendIfNotNull ("flag", m_sFlag)
453                                        .appendIfNotNull ("rich", m_aRich)
454                                        .appendIfNotNull ("linkable", m_aLinkable)
455                                        .append ("abstract", m_bAbstract)
456                                        .appendIfNotNull ("context", m_sContext)
457                                        .appendIfNotNull ("id", m_sID)
458                                        .appendIfNotEmpty ("includes", m_aIncludes)
459                                        .appendIfNotEmpty ("lets", m_aLets)
460                                        .appendIfNotEmpty ("content", m_aContent)
461                                        .appendIfNotEmpty ("foreignAttrs", m_aForeignAttrs)
462                                        .appendIfNotEmpty ("foreignElements", m_aForeignElements)
463                                        .toString ();
464   }
465 }