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.TransformerException;
30  import javax.xml.transform.TransformerFactory;
31  import javax.xml.transform.TransformerFactoryConfigurationError;
32  import javax.xml.transform.dom.DOMResult;
33  
34  import org.apache.logging.log4j.LogManager;
35  import org.apache.logging.log4j.Logger;
36  import org.apache.xalan.extensions.ExpressionContext;
37  import org.apache.xpath.NodeSet;
38  import org.apache.xpath.XPathContext;
39  import org.apache.xpath.objects.XNodeSet;
40  import org.apache.xpath.objects.XNodeSetForDOM;
41  import org.jdom2.Element;
42  import org.jdom2.JDOMException;
43  import org.jdom2.filter.ElementFilter;
44  import org.jdom2.transform.JDOMSource;
45  import org.jdom2.util.IteratorIterable;
46  import org.mycore.common.MCRUsageException;
47  import org.mycore.common.xml.MCRURIResolver;
48  import org.w3c.dom.Node;
49  import org.w3c.dom.NodeList;
50  
51  /**
52   * Handles xed:include, xed:preload and xed:modify|xed:extend to include XEditor components by URI and ID.
53   *
54   * @author Frank L\U00FCtzenkirchen
55   */
56  public class MCRIncludeHandler {
57  
58      private static final String ATTR_AFTER = "after";
59  
60      private static final String ATTR_BEFORE = "before";
61  
62      private static final String ATTR_ID = "id";
63  
64      private static final String ATTR_REF = "ref";
65  
66      private static final Logger LOGGER = LogManager.getLogger(MCRIncludeHandler.class);
67  
68      /** Caches preloaded components at application level: resolved only once, used many times (static) */
69      private static final Map<String, Element> CACHE_AT_APPLICATION_LEVEL = new ConcurrentHashMap<>();
70  
71      /** Caches preloaded components at transformation level, resolved on every reload of editor form page */
72      private Map<String, Element> cacheAtTransformationLevel = new HashMap<>();
73  
74      /**
75       * Preloads editor components from one or more URIs.
76       *
77       * @param uris a list of URIs to preload, separated by whitespace
78       * @param sStatic if true, use static cache on application level,
79       *               otherwise reload at each XEditor form transformation
80       */
81      public void preloadFromURIs(String uris, String sStatic)
82          throws 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 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 {
178             return "include".equals(e.getName()) && id.equals(e.getAttributeValue(ATTR_REF));
179         }
180     }
181 
182     private void handleInclude(Element container, Element includeRule) {
183         boolean modified = handleBeforeAfter(container, includeRule, ATTR_BEFORE, 0, 0);
184         if (!modified) {
185             includeRule.setAttribute(ATTR_AFTER, includeRule.getAttributeValue(ATTR_AFTER, "*"));
186             handleBeforeAfter(container, includeRule, ATTR_AFTER, 1, container.getChildren().size());
187         }
188     }
189 
190     private boolean handleBeforeAfter(Element container, Element includeRule, String attributeName, int offset,
191         int defaultPos) {
192         String refID = includeRule.getAttributeValue(attributeName);
193         if (refID != null) {
194             includeRule.removeAttribute(attributeName);
195 
196             Element parent = container;
197             int pos = defaultPos;
198 
199             Optional<Element> neighbor = findDescendant(container, refID);
200             if (neighbor.isPresent()) {
201                 Element n = neighbor.get();
202                 parent = n.getParentElement();
203                 List<Element> children = parent.getChildren();
204                 pos = children.indexOf(n) + offset;
205             }
206 
207             LOGGER.debug("including  " + Arrays.toString(includeRule.getAttributes().toArray()) + " at pos " + pos);
208             parent.getChildren().add(pos, includeRule.clone());
209         }
210         return refID != null;
211     }
212 
213     public XNodeSet resolve(ExpressionContext context, String ref) throws JDOMException, TransformerException {
214         LOGGER.debug("including component " + ref);
215         Map<String, Element> cache = chooseCacheLevel(ref, Boolean.FALSE.toString());
216         Element resolved = cache.get(ref);
217         return (resolved == null ? null : asNodeSet(context, jdom2dom(resolved)));
218     }
219 
220     public XNodeSet resolve(ExpressionContext context, String uri, String sStatic)
221         throws TransformerException, JDOMException {
222         LOGGER.debug("including xml " + uri);
223 
224         Element xml = resolve(uri, sStatic);
225         Node node = jdom2dom(xml);
226 
227         try {
228             return asNodeSet(context, node);
229         } catch (Exception ex) {
230             LOGGER.error(ex);
231             throw ex;
232         }
233     }
234 
235     private Element resolve(String uri, String sStatic)
236         throws TransformerException, TransformerFactoryConfigurationError {
237         Map<String, Element> cache = chooseCacheLevel(uri, sStatic);
238 
239         if (cache.containsKey(uri)) {
240             LOGGER.debug("uri was cached: " + uri);
241             return cache.get(uri);
242         } else {
243             Element xml = MCRURIResolver.instance().resolve(uri);
244             cache.put(uri, xml);
245             return xml;
246         }
247     }
248 
249     private Map<String, Element> chooseCacheLevel(String key, String sStatic) {
250         if ("true".equals(sStatic) || CACHE_AT_APPLICATION_LEVEL.containsKey(key)) {
251             return CACHE_AT_APPLICATION_LEVEL;
252         } else {
253             return cacheAtTransformationLevel;
254         }
255     }
256 
257     private Node jdom2dom(Element element) throws TransformerException, JDOMException {
258         DOMResult result = new DOMResult();
259         JDOMSource source = new JDOMSource(element);
260         TransformerFactory.newInstance().newTransformer().transform(source, result);
261         return result.getNode();
262     }
263 
264     private XNodeSet asNodeSet(ExpressionContext context, Node node) throws TransformerException, JDOMException {
265         NodeSet nodeSet = new NodeSet();
266         nodeSet.addNode(node);
267         XPathContext xpc = context.getXPathContext();
268         return new XNodeSetForDOM((NodeList) nodeSet, xpc);
269     }
270 }