1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
53
54
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
69 private static final Map<String, Element> CACHE_AT_APPLICATION_LEVEL = new ConcurrentHashMap<>();
70
71
72 private Map<String, Element> cacheAtTransformationLevel = new HashMap<>();
73
74
75
76
77
78
79
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
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);
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 }