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.frontend.xeditor;
20  
21  import java.util.Arrays;
22  import java.util.HashMap;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Optional;
26  import java.util.concurrent.ConcurrentHashMap;
27  import java.util.stream.StreamSupport;
28  
29  import javax.xml.transform.TransformerConfigurationException;
30  import javax.xml.transform.TransformerException;
31  import javax.xml.transform.TransformerFactory;
32  import javax.xml.transform.TransformerFactoryConfigurationError;
33  import javax.xml.transform.dom.DOMResult;
34  
35  import org.apache.logging.log4j.LogManager;
36  import org.apache.logging.log4j.Logger;
37  import org.apache.xalan.extensions.ExpressionContext;
38  import org.apache.xpath.NodeSet;
39  import org.apache.xpath.XPathContext;
40  import org.apache.xpath.objects.XNodeSet;
41  import org.apache.xpath.objects.XNodeSetForDOM;
42  import org.jdom2.Element;
43  import org.jdom2.JDOMException;
44  import org.jdom2.filter.ElementFilter;
45  import org.jdom2.transform.JDOMSource;
46  import org.jdom2.util.IteratorIterable;
47  import org.mycore.common.MCRUsageException;
48  import org.mycore.common.xml.MCRURIResolver;
49  import org.w3c.dom.Node;
50  import org.w3c.dom.NodeList;
51  
52  /**
53   * Handles xed:include, xed:preload and xed:modify|xed:extend to include XEditor components by URI and ID.
54   *
55   * @author Frank L\U00FCtzenkirchen
56   */
57  public class MCRIncludeHandler {
58  
59      private static final String ATTR_AFTER = "after";
60  
61      private static final String ATTR_BEFORE = "before";
62  
63      private static final String ATTR_ID = "id";
64  
65      private static final String ATTR_REF = "ref";
66  
67      private static final Logger LOGGER = LogManager.getLogger(MCRIncludeHandler.class);
68  
69      /** Caches preloaded components at application level: resolved only once, used many times (static) */
70      private static final Map<String, Element> CACHE_AT_APPLICATION_LEVEL = new ConcurrentHashMap<>();
71  
72      /** Caches preloaded components at transformation level, resolved on every reload of editor form page */
73      private Map<String, Element> cacheAtTransformationLevel = new HashMap<>();
74  
75      /**
76       * Preloads editor components from one or more URIs.
77       *
78       * @param uris a list of URIs to preload, separated by whitespace
79       * @param sStatic if true, use static cache on application level, otherwise reload at each XEditor form transformation
80       */
81      public void preloadFromURIs(String uris, String sStatic)
82          throws TransformerConfigurationException, TransformerException, TransformerFactoryConfigurationError {
83          for (String uri : uris.split(",")) {
84              preloadFromURI(uri, sStatic);
85          }
86      }
87  
88      private void preloadFromURI(String uri, String sStatic)
89          throws TransformerConfigurationException, TransformerException, TransformerFactoryConfigurationError {
90          if (uri.trim().isEmpty()) {
91              return;
92          }
93  
94          LOGGER.debug("preloading " + uri);
95  
96          Element xml;
97          try {
98              xml = resolve(uri.trim(), sStatic);
99          } catch (Exception ex) {
100             LOGGER.warn("Exception preloading " + uri, ex);
101             return;
102         }
103 
104         Map<String, Element> cache = chooseCacheLevel(uri, sStatic);
105         handlePreloadedComponents(xml, cache);
106     }
107 
108     /**
109      * Cache all descendant components that have an @id, handle xed:modify|xed:extend afterwards
110      */
111     private void handlePreloadedComponents(Element xml, Map<String, Element> cache) {
112         for (Element component : xml.getChildren()) {
113             cacheComponent(cache, component);
114             handlePreloadedComponents(component, cache);
115             handleModify(cache, component);
116         }
117     }
118 
119     private void cacheComponent(Map<String, Element> cache, Element element) {
120         String id = element.getAttributeValue(ATTR_ID);
121         if ((id != null) && !id.isEmpty()) {
122             LOGGER.debug("preloaded component " + id);
123             cache.put(id, element);
124         }
125     }
126 
127     private void handleModify(Map<String, Element> cache, Element element) {
128         if ("modify".equals(element.getName())) {
129             String refID = element.getAttributeValue(ATTR_REF);
130             if (refID == null) {
131                 throw new MCRUsageException("<xed:modify /> must have a @ref attribute!");
132             }
133 
134             Element container = cache.get(refID);
135             if (container == null) {
136                 LOGGER.warn("Ignoring xed:modify of " + refID + ", no component with that @id found");
137                 return;
138             }
139 
140             container = container.clone();
141 
142             String newID = element.getAttributeValue(ATTR_ID);
143             if (newID != null) {
144                 container.setAttribute(ATTR_ID, newID); // extend rather that modify
145                 LOGGER.debug("extending " + refID + " to " + newID);
146             } else {
147                 LOGGER.debug("modifying " + refID);
148             }
149 
150             for (Element command : element.getChildren()) {
151                 String commandType = command.getName();
152                 if ("remove".equals(commandType)) {
153                     handleRemove(container, command);
154                 } else if ("include".equals(commandType)) {
155                     handleInclude(container, command);
156                 }
157             }
158             cacheComponent(cache, container);
159         }
160     }
161 
162     private void handleRemove(Element container, Element removeRule) {
163         String id = removeRule.getAttributeValue(ATTR_REF);
164         LOGGER.debug("removing " + id);
165         findDescendant(container, id).ifPresent(e -> e.detach());
166     }
167 
168     private Optional<Element> findDescendant(Element container, String id) {
169         IteratorIterable<Element> descendants = container.getDescendants(new ElementFilter());
170         return StreamSupport.stream(descendants.spliterator(), false)
171             .filter(e -> hasOrIncludesID(e, id)).findFirst();
172     }
173 
174     private boolean hasOrIncludesID(Element e, String id) {
175         if (id.equals(e.getAttributeValue(ATTR_ID))) {
176             return true;
177         } else if ("include".equals(e.getName()) && id.equals(e.getAttributeValue(ATTR_REF))) {
178             return true;
179         } else {
180             return false;
181         }
182     }
183 
184     private void handleInclude(Element container, Element includeRule) {
185         boolean modified = handleBeforeAfter(container, includeRule, ATTR_BEFORE, 0, 0);
186         if (!modified) {
187             includeRule.setAttribute(ATTR_AFTER, includeRule.getAttributeValue(ATTR_AFTER, "*"));
188             handleBeforeAfter(container, includeRule, ATTR_AFTER, 1, container.getChildren().size());
189         }
190     }
191 
192     private boolean handleBeforeAfter(Element container, Element includeRule, String attributeName, int offset,
193         int defaultPos) {
194         String refID = includeRule.getAttributeValue(attributeName);
195         if (refID != null) {
196             includeRule.removeAttribute(attributeName);
197 
198             Element parent = container;
199             int pos = defaultPos;
200 
201             Optional<Element> neighbor = findDescendant(container, refID);
202             if (neighbor.isPresent()) {
203                 Element n = neighbor.get();
204                 parent = n.getParentElement();
205                 List<Element> children = parent.getChildren();
206                 pos = children.indexOf(n) + offset;
207             }
208 
209             LOGGER.debug("including  " + Arrays.toString(includeRule.getAttributes().toArray()) + " at pos " + pos);
210             parent.getChildren().add(pos, includeRule.clone());
211         }
212         return refID != null;
213     }
214 
215     public XNodeSet resolve(ExpressionContext context, String ref) throws JDOMException, TransformerException {
216         LOGGER.debug("including component " + ref);
217         Map<String, Element> cache = chooseCacheLevel(ref, Boolean.FALSE.toString());
218         Element resolved = cache.get(ref);
219         return (resolved == null ? null : asNodeSet(context, jdom2dom(resolved)));
220     }
221 
222     public XNodeSet resolve(ExpressionContext context, String uri, String sStatic)
223         throws TransformerException, JDOMException {
224         LOGGER.debug("including xml " + uri);
225 
226         Element xml = resolve(uri, sStatic);
227         Node node = jdom2dom(xml);
228 
229         try {
230             return asNodeSet(context, node);
231         } catch (Exception ex) {
232             LOGGER.error(ex);
233             throw ex;
234         }
235     }
236 
237     private Element resolve(String uri, String sStatic)
238         throws TransformerConfigurationException, TransformerException, TransformerFactoryConfigurationError {
239         Map<String, Element> cache = chooseCacheLevel(uri, sStatic);
240 
241         if (cache.containsKey(uri)) {
242             LOGGER.debug("uri was cached: " + uri);
243             return cache.get(uri);
244         } else {
245             Element xml = MCRURIResolver.instance().resolve(uri);
246             cache.put(uri, xml);
247             return xml;
248         }
249     }
250 
251     private Map<String, Element> chooseCacheLevel(String key, String sStatic) {
252         if ("true".equals(sStatic) || CACHE_AT_APPLICATION_LEVEL.containsKey(key)) {
253             return CACHE_AT_APPLICATION_LEVEL;
254         } else {
255             return cacheAtTransformationLevel;
256         }
257     }
258 
259     private Node jdom2dom(Element element) throws TransformerException, JDOMException {
260         DOMResult result = new DOMResult();
261         JDOMSource source = new JDOMSource(element);
262         TransformerFactory.newInstance().newTransformer().transform(source, result);
263         return result.getNode();
264     }
265 
266     private XNodeSet asNodeSet(ExpressionContext context, Node node) throws TransformerException, JDOMException {
267         NodeSet nodeSet = new NodeSet();
268         nodeSet.addNode(node);
269         XPathContext xpc = context.getXPathContext();
270         return new XNodeSetForDOM((NodeList) nodeSet, xpc);
271     }
272 }