View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.datamodel.metadata.validator;
20  
21  import static org.jdom2.Namespace.XML_NAMESPACE;
22  import static org.mycore.common.MCRConstants.XLINK_NAMESPACE;
23  import static org.mycore.common.MCRConstants.XSI_NAMESPACE;
24  
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.HashMap;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Map.Entry;
34  import java.util.stream.Collectors;
35  
36  import org.apache.logging.log4j.LogManager;
37  import org.apache.logging.log4j.Logger;
38  import org.jdom2.Attribute;
39  import org.jdom2.Document;
40  import org.jdom2.Element;
41  import org.jdom2.JDOMException;
42  import org.jdom2.filter.Filters;
43  import org.jdom2.input.SAXBuilder;
44  import org.jdom2.output.Format;
45  import org.jdom2.output.XMLOutputter;
46  import org.jdom2.xpath.XPathFactory;
47  import org.mycore.access.MCRAccessManager;
48  import org.mycore.access.MCRRuleAccessInterface;
49  import org.mycore.common.MCRClassTools;
50  import org.mycore.common.MCRException;
51  import org.mycore.common.MCRSessionMgr;
52  import org.mycore.common.MCRUtils;
53  import org.mycore.common.config.MCRConfiguration2;
54  import org.mycore.common.content.MCRJDOMContent;
55  import org.mycore.datamodel.metadata.MCRMetaAccessRule;
56  import org.mycore.datamodel.metadata.MCRMetaAddress;
57  import org.mycore.datamodel.metadata.MCRMetaBoolean;
58  import org.mycore.datamodel.metadata.MCRMetaClassification;
59  import org.mycore.datamodel.metadata.MCRMetaDerivateLink;
60  import org.mycore.datamodel.metadata.MCRMetaEnrichedLinkID;
61  import org.mycore.datamodel.metadata.MCRMetaHistoryDate;
62  import org.mycore.datamodel.metadata.MCRMetaISO8601Date;
63  import org.mycore.datamodel.metadata.MCRMetaInstitutionName;
64  import org.mycore.datamodel.metadata.MCRMetaInterface;
65  import org.mycore.datamodel.metadata.MCRMetaLangText;
66  import org.mycore.datamodel.metadata.MCRMetaLink;
67  import org.mycore.datamodel.metadata.MCRMetaLinkID;
68  import org.mycore.datamodel.metadata.MCRMetaNumber;
69  import org.mycore.datamodel.metadata.MCRMetaPersonName;
70  import org.mycore.datamodel.metadata.MCRObject;
71  import org.mycore.datamodel.metadata.MCRObjectID;
72  import org.xml.sax.SAXParseException;
73  
74  /**
75   * @author Thomas Scheffler (yagee)
76   * @version $Revision: 1 $ $Date: 08.05.2009 15:51:37 $
77   */
78  public class MCREditorOutValidator {
79  
80      private static final String CONFIG_PREFIX = "MCR.EditorOutValidator.";
81  
82      private static final SAXBuilder SAX_BUILDER = new SAXBuilder();
83  
84      private static Logger LOGGER = LogManager.getLogger();
85  
86      private static Map<String, MCREditorMetadataValidator> VALIDATOR_MAP = getValidatorMap();
87  
88      private static Map<String, Class<? extends MCRMetaInterface>> CLASS_MAP = new HashMap<>();
89  
90      private Document input;
91  
92      private MCRObjectID id;
93  
94      private List<String> errorlog;
95  
96      /**
97       * instantiate the validator with the editor input <code>jdom_in</code>.
98       * 
99       * <code>id</code> will be set as the MCRObjectID for the resulting object
100      * that can be fetched with <code>generateValidMyCoReObject()</code>
101      * 
102      * @param jdomIn
103      *            editor input
104      */
105     public MCREditorOutValidator(Document jdomIn, MCRObjectID id) throws JDOMException, IOException {
106         errorlog = new ArrayList<>();
107         input = jdomIn;
108         this.id = id;
109         if (LOGGER.isDebugEnabled()) {
110             LOGGER.debug("XML before validation:\n{}", new XMLOutputter(Format.getPrettyFormat()).outputString(input));
111         }
112         checkObject();
113         if (LOGGER.isDebugEnabled()) {
114             LOGGER.debug("XML after validation:\n{}", new XMLOutputter(Format.getPrettyFormat()).outputString(input));
115         }
116     }
117 
118     private static Map<String, MCREditorMetadataValidator> getValidatorMap() {
119         Map<String, MCREditorMetadataValidator> map = new HashMap<>();
120         map.put(MCRMetaBoolean.class.getSimpleName(), getObjectCheckInstance(MCRMetaBoolean.class));
121         map.put(MCRMetaPersonName.class.getSimpleName(), getObjectCheckWithLangInstance(MCRMetaPersonName.class));
122         map.put(MCRMetaInstitutionName.class.getSimpleName(),
123             getObjectCheckWithLangInstance(MCRMetaInstitutionName.class));
124         map.put(MCRMetaAddress.class.getSimpleName(), new MCRMetaAdressCheck());
125         map.put(MCRMetaNumber.class.getSimpleName(), getObjectCheckWithLangNotEmptyInstance(MCRMetaNumber.class));
126         map.put(MCRMetaLinkID.class.getSimpleName(), getObjectCheckWithLinksInstance(MCRMetaLinkID.class));
127         map.put(MCRMetaEnrichedLinkID.class.getSimpleName(),
128             getObjectCheckWithLinksInstance(MCRMetaEnrichedLinkID.class));
129         map.put(MCRMetaDerivateLink.class.getSimpleName(), getObjectCheckWithLinksInstance(MCRMetaDerivateLink.class));
130         map.put(MCRMetaLink.class.getSimpleName(), getObjectCheckWithLinksInstance(MCRMetaLink.class));
131         map.put(MCRMetaISO8601Date.class.getSimpleName(),
132             getObjectCheckWithLangNotEmptyInstance(MCRMetaISO8601Date.class));
133         map.put(MCRMetaLangText.class.getSimpleName(), getObjectCheckWithLangNotEmptyInstance(MCRMetaLangText.class));
134         map.put(MCRMetaAccessRule.class.getSimpleName(), getObjectCheckInstance(MCRMetaAccessRule.class));
135         map.put(MCRMetaClassification.class.getSimpleName(), new MCRMetaClassificationCheck());
136         map.put(MCRMetaHistoryDate.class.getSimpleName(), new MCRMetaHistoryDateCheck());
137         Map<String, String> props = MCRConfiguration2.getPropertiesMap()
138             .entrySet()
139             .stream()
140             .filter(p -> p.getKey().startsWith(CONFIG_PREFIX + "class."))
141             .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
142         for (Entry<String, String> entry : props.entrySet()) {
143             try {
144                 String className = entry.getKey();
145                 className = className.substring(className.lastIndexOf('.') + 1);
146                 LOGGER.info("Adding Validator {} for class {}", entry.getValue(), className);
147                 @SuppressWarnings("unchecked")
148                 Class<? extends MCREditorMetadataValidator> cl = (Class<? extends MCREditorMetadataValidator>) Class
149                     .forName(entry.getValue());
150                 map.put(className, cl.getDeclaredConstructor().newInstance());
151             } catch (Exception e) {
152                 final String msg = "Cannot instantiate " + entry.getValue() + " as validator for class "
153                     + entry.getKey();
154                 LOGGER.error(msg);
155                 throw new MCRException(msg, e);
156             }
157         }
158         return map;
159     }
160 
161     @SuppressWarnings("unchecked")
162     public static Class<? extends MCRMetaInterface> getClass(String mcrclass) throws ClassNotFoundException {
163         Class<? extends MCRMetaInterface> clazz = CLASS_MAP.get(mcrclass);
164         if (clazz == null) {
165             clazz = MCRClassTools.forName("org.mycore.datamodel.metadata." + mcrclass);
166             CLASS_MAP.put(mcrclass, clazz);
167         }
168         return clazz;
169     }
170 
171     public static String checkMetaObject(Element datasubtag, Class<? extends MCRMetaInterface> metaClass,
172         boolean keepLang) {
173         if (!keepLang) {
174             datasubtag.removeAttribute("lang", XML_NAMESPACE);
175         }
176         MCRMetaInterface test = null;
177         try {
178             test = metaClass.getDeclaredConstructor().newInstance();
179         } catch (Exception e) {
180             throw new MCRException("Could not instantiate " + metaClass.getCanonicalName());
181         }
182         test.setFromDOM(datasubtag);
183         test.validate();
184         return null;
185     }
186 
187     public static String checkMetaObjectWithLang(Element datasubtag, Class<? extends MCRMetaInterface> metaClass) {
188         if (datasubtag.getAttribute("lang") != null) {
189             datasubtag.getAttribute("lang").setNamespace(XML_NAMESPACE);
190             LOGGER.warn("namespace add for xml:lang attribute in {}", datasubtag.getName());
191         }
192         return checkMetaObject(datasubtag, metaClass, true);
193     }
194 
195     public static String checkMetaObjectWithLangNotEmpty(Element datasubtag,
196         Class<? extends MCRMetaInterface> metaClass) {
197         String text = datasubtag.getTextTrim();
198         if (text == null || text.length() == 0) {
199             return "Element " + datasubtag.getName() + " has no text.";
200         }
201         return checkMetaObjectWithLang(datasubtag, metaClass);
202     }
203 
204     public static String checkMetaObjectWithLinks(Element datasubtag, Class<? extends MCRMetaInterface> metaClass) {
205         if (datasubtag.getAttributeValue("href") == null
206             && datasubtag.getAttributeValue("href", XLINK_NAMESPACE) == null) {
207             return datasubtag.getName() + " has no href attribute defined";
208         }
209         if (datasubtag.getAttribute("xtype") != null) {
210             datasubtag.getAttribute("xtype").setNamespace(XLINK_NAMESPACE).setName("type");
211         } else if (datasubtag.getAttribute("type") != null
212             && datasubtag.getAttribute("type", XLINK_NAMESPACE) == null) {
213             datasubtag.getAttribute("type").setNamespace(XLINK_NAMESPACE);
214             LOGGER.warn("namespace add for xlink:type attribute in {}", datasubtag.getName());
215         }
216         if (datasubtag.getAttribute("href") != null) {
217             datasubtag.getAttribute("href").setNamespace(XLINK_NAMESPACE);
218             LOGGER.warn("namespace add for xlink:href attribute in {}", datasubtag.getName());
219         }
220 
221         if (datasubtag.getAttribute("title") != null) {
222             datasubtag.getAttribute("title").setNamespace(XLINK_NAMESPACE);
223             LOGGER.warn("namespace add for xlink:title attribute in {}", datasubtag.getName());
224         }
225 
226         if (datasubtag.getAttribute("label") != null) {
227             datasubtag.getAttribute("label").setNamespace(XLINK_NAMESPACE);
228             LOGGER.warn("namespace add for xlink:label attribute in {}", datasubtag.getName());
229         }
230         return checkMetaObject(datasubtag, metaClass, false);
231     }
232 
233     static MCREditorMetadataValidator getObjectCheckInstance(final Class<? extends MCRMetaInterface> clazz) {
234         return datasubtag -> MCREditorOutValidator.checkMetaObject(datasubtag, clazz, false);
235     }
236 
237     static MCREditorMetadataValidator getObjectCheckWithLangInstance(final Class<? extends MCRMetaInterface> clazz) {
238         return datasubtag -> MCREditorOutValidator.checkMetaObjectWithLang(datasubtag, clazz);
239     }
240 
241     static MCREditorMetadataValidator getObjectCheckWithLangNotEmptyInstance(
242         final Class<? extends MCRMetaInterface> clazz) {
243         return datasubtag -> MCREditorOutValidator.checkMetaObjectWithLangNotEmpty(datasubtag, clazz);
244     }
245 
246     static MCREditorMetadataValidator getObjectCheckWithLinksInstance(final Class<? extends MCRMetaInterface> clazz) {
247         return datasubtag -> MCREditorOutValidator.checkMetaObjectWithLinks(datasubtag, clazz);
248     }
249 
250     /**
251      * The method add a default ACL-block.
252      */
253     public static void setDefaultDerivateACLs(Element service) {
254         // Read stylesheet and add user
255         InputStream aclxml = MCREditorOutValidator.class.getResourceAsStream("/editor_default_acls_derivate.xml");
256         if (aclxml == null) {
257             LOGGER.warn("Can't find default derivate ACL file editor_default_acls_derivate.xml.");
258             return;
259         }
260         try {
261             Document xml = SAX_BUILDER.build(aclxml);
262             Element acls = xml.getRootElement().getChild("servacls");
263             if (acls != null) {
264                 service.addContent(acls.detach());
265             }
266         } catch (Exception e) {
267             LOGGER.warn("Error while parsing file editor_default_acls_derivate.xml.");
268         }
269     }
270 
271     /**
272      * tries to generate a valid MCRObject as JDOM Document.
273      *
274      * @return MCRObject
275      */
276     public Document generateValidMyCoReObject() throws JDOMException, SAXParseException, IOException {
277         MCRObject obj;
278         // load the JDOM object
279         XPathFactory.instance()
280             .compile("/mycoreobject/*/*/*/@editor.output", Filters.attribute())
281             .evaluate(input)
282             .forEach(Attribute::detach);
283         try {
284             byte[] xml = new MCRJDOMContent(input).asByteArray();
285             obj = new MCRObject(xml, true);
286         } catch (SAXParseException e) {
287             XMLOutputter xout = new XMLOutputter(Format.getPrettyFormat());
288             LOGGER.warn("Failure while parsing document:\n{}", xout.outputString(input));
289             throw e;
290         }
291         // remove that, because its set in MCRMetadataManager, and we need the information (MCR-2603).
292         //Date curTime = new Date();
293         //obj.getService().setDate("modifydate", curTime);
294 
295         // return the XML tree
296         input = obj.createXML();
297         return input;
298     }
299 
300     /**
301      * returns a List of Error log entries
302      *
303      * @return log entries for the whole validation process
304      */
305     public List<String> getErrorLog() {
306         return errorlog;
307     }
308 
309     /**
310      * @throws IOException
311      * @throws JDOMException
312      *
313      */
314     private void checkObject() throws JDOMException, IOException {
315         // add the namespaces (this is a workaround)
316         Element root = input.getRootElement();
317         root.addNamespaceDeclaration(XLINK_NAMESPACE);
318         root.addNamespaceDeclaration(XSI_NAMESPACE);
319         // set the schema
320         String mcrSchema = "datamodel-" + id.getTypeId() + ".xsd";
321         root.setAttribute("noNamespaceSchemaLocation", mcrSchema, XSI_NAMESPACE);
322         // check the label
323         String label = MCRUtils.filterTrimmedNotEmpty(root.getAttributeValue("label"))
324             .orElse(null);
325         if (label == null) {
326             root.setAttribute("label", id.toString());
327         }
328         // remove the path elements from the incoming
329         Element pathes = root.getChild("pathes");
330         if (pathes != null) {
331             root.removeChildren("pathes");
332         }
333         Element structure = root.getChild("structure");
334         if (structure == null) {
335             root.addContent(new Element("structure"));
336         } else {
337             checkObjectStructure(structure);
338         }
339         Element metadata = root.getChild("metadata");
340         checkObjectMetadata(metadata);
341         Element service = root.getChild("service");
342         checkObjectService(root, service);
343     }
344 
345     /**
346      * @param datatag
347      */
348     private boolean checkMetaTags(Element datatag) {
349         String mcrclass = datatag.getAttributeValue("class");
350         List<Element> datataglist = datatag.getChildren();
351         Iterator<Element> datatagIt = datataglist.iterator();
352 
353         while (datatagIt.hasNext()) {
354             Element datasubtag = datatagIt.next();
355             MCREditorMetadataValidator validator = VALIDATOR_MAP.get(mcrclass);
356             String returns = null;
357             if (validator != null) {
358                 returns = validator.checkDataSubTag(datasubtag);
359             } else {
360                 LOGGER.warn("Tag <{}> of type {} has no validator defined, fallback to default behaviour",
361                     datatag.getName(), mcrclass);
362                 // try to create MCRMetaInterface instance
363                 try {
364                     Class<? extends MCRMetaInterface> metaClass = getClass(mcrclass);
365                     // just checks if class would validate this element
366                     returns = checkMetaObject(datasubtag, metaClass, true);
367                 } catch (ClassNotFoundException e) {
368                     throw new MCRException("Failure while trying fallback. Class not found: " + mcrclass, e);
369                 }
370             }
371             if (returns != null) {
372                 datatagIt.remove();
373                 final String msg = datatag.getName() + ": " + returns;
374                 errorlog.add(msg);
375             }
376         }
377         return datatag.getChildren().size() != 0;
378     }
379 
380     /**
381      * @param service
382      * @throws IOException
383      * @throws JDOMException
384      */
385     private void checkObjectService(Element root, Element service) throws JDOMException, IOException {
386         if (service == null) {
387             service = new Element("service");
388             root.addContent(service);
389         }
390         List<Element> servicelist = service.getChildren();
391         for (Element datatag : servicelist) {
392             checkMetaTags(datatag);
393         }
394 
395         if (service.getChild("servacls") == null &&
396             MCRAccessManager.getAccessImpl() instanceof MCRRuleAccessInterface) {
397             Collection<String> li = MCRAccessManager.getPermissionsForID(id.toString());
398             if (li == null || li.isEmpty()) {
399                 setDefaultObjectACLs(service);
400             }
401         }
402     }
403 
404     /**
405      * The method add a default ACL-block.
406      *
407      * @param service
408      * @throws IOException
409      * @throws JDOMException
410      */
411     private void setDefaultObjectACLs(Element service) throws JDOMException, IOException {
412         if (!MCRConfiguration2.getBoolean("MCR.Access.AddObjectDefaultRule").orElse(true)) {
413             LOGGER.info("Adding object default acl rule is disabled.");
414             return;
415         }
416         String resourcetype = "/editor_default_acls_" + id.getTypeId() + ".xml";
417         String resourcebase = "/editor_default_acls_" + id.getBase() + ".xml";
418         // Read stylesheet and add user
419         InputStream aclxml = MCREditorOutValidator.class.getResourceAsStream(resourcebase);
420         if (aclxml == null) {
421             aclxml = MCREditorOutValidator.class.getResourceAsStream(resourcetype);
422             if (aclxml == null) {
423                 LOGGER.warn("Can't find default object ACL file {} or {}", resourcebase.substring(1),
424                     resourcetype.substring(1));
425                 String resource = "/editor_default_acls.xml"; // fallback
426                 aclxml = MCREditorOutValidator.class.getResourceAsStream(resource);
427                 if (aclxml == null) {
428                     return;
429                 }
430             }
431         }
432         Document xml = SAX_BUILDER.build(aclxml);
433         Element acls = xml.getRootElement().getChild("servacls");
434         if (acls == null) {
435             return;
436         }
437         for (Element acl : acls.getChildren()) {
438             Element condition = acl.getChild("condition");
439             if (condition == null) {
440                 continue;
441             }
442             Element rootbool = condition.getChild("boolean");
443             if (rootbool == null) {
444                 continue;
445             }
446             for (Element orbool : rootbool.getChildren("boolean")) {
447                 for (Element firstcond : orbool.getChildren("condition")) {
448                     if (firstcond == null) {
449                         continue;
450                     }
451                     String value = firstcond.getAttributeValue("value");
452                     if (value == null) {
453                         continue;
454                     }
455                     if (value.equals("$CurrentUser")) {
456                         String thisuser = MCRSessionMgr.getCurrentSession().getUserInformation().getUserID();
457                         firstcond.setAttribute("value", thisuser);
458                         continue;
459                     }
460                     if (value.equals("$CurrentGroup")) {
461                         throw new MCRException(
462                             "The parameter $CurrentGroup in default ACLs is not supported as of MyCoRe 2014.06"
463                                 + " because it is not supported in Servlet API 3.0");
464                     }
465                     int i = value.indexOf("$CurrentIP");
466                     if (i != -1) {
467                         String thisip = MCRSessionMgr.getCurrentSession().getCurrentIP();
468                         firstcond.setAttribute("value",
469                             value.substring(0, i) + thisip + value.substring(i + 10));
470                     }
471                 }
472             }
473         }
474         service.addContent(acls.detach());
475     }
476 
477     /**
478      * @param metadata
479      */
480     private void checkObjectMetadata(Element metadata) {
481         if (metadata.getAttribute("lang") != null) {
482             metadata.getAttribute("lang").setNamespace(XML_NAMESPACE);
483         }
484 
485         List<Element> metadatalist = metadata.getChildren();
486         Iterator<Element> metaIt = metadatalist.iterator();
487 
488         while (metaIt.hasNext()) {
489             Element datatag = metaIt.next();
490             if (!checkMetaTags(datatag)) {
491                 // e.g. datatag is empty
492                 LOGGER.debug("Removing element :{}", datatag.getName());
493                 metaIt.remove();
494             }
495         }
496     }
497 
498     private void checkObjectStructure(Element structure) {
499         // e.g. datatag is empty
500         structure.getChildren().removeIf(datatag -> !checkMetaTags(datatag));
501     }
502 
503     static class MCRMetaHistoryDateCheck implements MCREditorMetadataValidator {
504         public String checkDataSubTag(Element datasubtag) {
505             Element[] children = datasubtag.getChildren("text").toArray(Element[]::new);
506             int textCount = children.length;
507             for (int i = 0; i < children.length; i++) {
508                 Element child = children[i];
509                 String text = child.getTextTrim();
510                 if (text == null || text.length() == 0) {
511                     child.detach();
512                     textCount--;
513                     continue;
514                 }
515                 if (child.getAttribute("lang") != null) {
516                     child.getAttribute("lang").setNamespace(XML_NAMESPACE);
517                     LOGGER.warn("namespace add for xml:lang attribute in {}", datasubtag.getName());
518                 }
519             }
520             if (textCount == 0) {
521                 return "history date is empty";
522             }
523             return checkMetaObjectWithLang(datasubtag, MCRMetaHistoryDate.class);
524         }
525     }
526 
527     static class MCRMetaClassificationCheck implements MCREditorMetadataValidator {
528         public String checkDataSubTag(Element datasubtag) {
529             String categid = datasubtag.getAttributeValue("categid");
530             if (categid == null) {
531                 return "Attribute categid is empty";
532             }
533             return checkMetaObject(datasubtag, MCRMetaClassification.class, false);
534         }
535     }
536 
537     static class MCRMetaAdressCheck implements MCREditorMetadataValidator {
538         public String checkDataSubTag(Element datasubtag) {
539             if (datasubtag.getChildren().size() == 0) {
540                 return "adress is empty";
541             }
542             return checkMetaObjectWithLang(datasubtag, MCRMetaAddress.class);
543         }
544     }
545 
546     static class MCRMetaPersonNameCheck implements MCREditorMetadataValidator {
547         public String checkDataSubTag(Element datasubtag) {
548             if (datasubtag.getChildren().size() == 0) {
549                 return "person name is empty";
550             }
551             return checkMetaObjectWithLang(datasubtag, MCRMetaAddress.class);
552         }
553     }
554 
555 }