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.common.xsl;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.UncheckedIOException;
25  import java.nio.file.FileVisitResult;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.SimpleFileVisitor;
29  import java.nio.file.attribute.BasicFileAttributes;
30  import java.util.ArrayList;
31  import java.util.HashMap;
32  import java.util.HashSet;
33  import java.util.LinkedList;
34  import java.util.List;
35  import java.util.Locale;
36  import java.util.Map;
37  import java.util.Map.Entry;
38  import java.util.Set;
39  import java.util.function.Consumer;
40  import java.util.function.Predicate;
41  import java.util.zip.ZipEntry;
42  import java.util.zip.ZipInputStream;
43  
44  import javax.xml.transform.TransformerException;
45  
46  import org.apache.logging.log4j.LogManager;
47  import org.apache.logging.log4j.Logger;
48  import org.jdom2.Element;
49  import org.jdom2.filter.Filters;
50  import org.jdom2.util.IteratorIterable;
51  import org.mycore.common.MCRConstants;
52  import org.mycore.common.config.MCRConfigurationDir;
53  import org.mycore.common.content.MCRJDOMContent;
54  import org.mycore.common.xml.MCRURIResolver;
55  import org.mycore.frontend.servlets.MCRServlet;
56  import org.mycore.frontend.servlets.MCRServletJob;
57  import org.xml.sax.SAXException;
58  
59  /**
60   * Lists all *.xsl stylesheets in the web application located in any 
61   * WEB-INF/lib/*.jar or WEB-INF/classes/xsl/ or in {@link MCRConfigurationDir}, outputs the
62   * dependencies (import/include) and contained templates.
63   * 
64   * @author Frank L\u00FCtzenkirchen
65   */
66  public final class MCRXSLInfoServlet extends MCRServlet {
67  
68      private static final Logger LOGGER = LogManager.getLogger(MCRXSLInfoServlet.class);
69  
70      private final Map<String, Stylesheet> stylesheets = new HashMap<>();
71  
72      private final Set<String> unknown = new HashSet<>();
73  
74      protected void doGetPost(MCRServletJob job) throws Exception {
75          if ("true".equals(job.getRequest().getParameter("reload"))) {
76              stylesheets.clear();
77          }
78  
79          if (stylesheets.isEmpty()) {
80              LOGGER.info("Collecting stylesheet information....");
81              findInConfigDir();
82              findXSLinClassesDir();
83              findXSLinLibJars();
84              inspectStylesheets();
85              handleUnknownStylesheets();
86          }
87  
88          buildOutput(job);
89      }
90  
91      private void findInConfigDir() throws IOException {
92          final File configDir = MCRConfigurationDir.getConfigurationDirectory();
93          if (configDir == null) {
94              return;
95          }
96          findInConfigResourcesDir(configDir);
97          findInConfigLibDir(configDir);
98      }
99  
100     private void findInConfigResourcesDir(File configDir) throws IOException {
101         final Path resources = configDir.toPath().resolve("resources");
102         final Path xslResourcesPath = resources.resolve("xsl");
103         doForFilesRecursive(xslResourcesPath, n -> n.endsWith(".xsl"),
104             file -> foundStylesheet(resources.toUri().relativize(file.toUri()).toString(),
105                 configDir.toPath().relativize(file).toString()));
106     }
107 
108     private void findInConfigLibDir(File configDir) throws IOException {
109         final Path lib = configDir.toPath().resolve("lib");
110         try {
111             doForFilesRecursive(lib, n -> n.endsWith(".jar"),
112                 file -> {
113                     try (InputStream in = Files.newInputStream(file)) {
114                         findInJarInputStream(configDir.toPath().relativize(file).toString(), in);
115                     } catch (IOException e) {
116                         throw new UncheckedIOException(e);
117                     }
118                 });
119         } catch (UncheckedIOException uioe) {
120             throw uioe.getCause();
121         }
122     }
123 
124     private void doForFilesRecursive(Path baseDir, Predicate<String> lowerCaseCheck, Consumer<Path> pathConsumer)
125         throws IOException {
126         if (Files.isDirectory(baseDir)) {
127             Files.walkFileTree(baseDir, new SimpleFileVisitor<>() {
128                 @Override
129                 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
130                     if (lowerCaseCheck.test(file.getFileName().toString().toLowerCase(Locale.ROOT))) {
131                         pathConsumer.accept(file);
132                     }
133                     return super.visitFile(file, attrs);
134                 }
135             });
136         }
137     }
138 
139     private void inspectStylesheets() {
140         for (Entry<String, Stylesheet> entry : stylesheets.entrySet()) {
141             entry.getValue().inspect();
142         }
143     }
144 
145     private void handleUnknownStylesheets() {
146         while (!unknown.isEmpty()) {
147             Set<String> list = new HashSet<>(unknown);
148             unknown.clear();
149 
150             for (String name : list) {
151                 Stylesheet s = new Stylesheet(name);
152                 stylesheets.put(name, s);
153                 s.inspect();
154             }
155         }
156     }
157 
158     private void buildOutput(MCRServletJob job) throws IOException, TransformerException, SAXException {
159         Element output = new Element("stylesheets");
160         for (Entry<String, Stylesheet> entry : stylesheets.entrySet()) {
161             Stylesheet stylesheet = entry.getValue();
162             output.addContent(stylesheet.buildXML());
163         }
164         getLayoutService().doLayout(job.getRequest(), job.getResponse(), new MCRJDOMContent(output));
165     }
166 
167     private void findXSLinLibJars() throws IOException {
168         for (String path : diveInto("/WEB-INF/lib/")) {
169             if (path.endsWith(".jar")) {
170                 findXSLinJar(path);
171             }
172         }
173     }
174 
175     private Set<String> diveInto(String base) {
176         LOGGER.info("Diving into {}...", base);
177         Set<String> paths = getServletContext().getResourcePaths(base);
178 
179         Set<String> more = new HashSet<>();
180         if (paths != null) {
181             more.addAll(paths);
182 
183             for (String path : paths) {
184                 if (path.endsWith("/")) {
185                     more.addAll(diveInto(path));
186                 }
187             }
188         }
189 
190         return more;
191     }
192 
193     private void findXSLinJar(String pathOfJarFile) throws IOException {
194         LOGGER.info("Diving into {}...", pathOfJarFile);
195 
196         try (InputStream in = getServletContext().getResourceAsStream(pathOfJarFile)) {
197             findInJarInputStream(pathOfJarFile, in);
198         }
199     }
200 
201     private void findInJarInputStream(String pathOfJarFile, InputStream in) throws IOException {
202         try (ZipInputStream zis = new ZipInputStream(in)) {
203 
204             for (ZipEntry ze = zis.getNextEntry(); ze != null; ze = zis.getNextEntry()) {
205                 String name = ze.getName();
206                 if (name.startsWith("xsl/") && name.endsWith(".xsl")) {
207                     foundStylesheet(name, pathOfJarFile);
208                 }
209                 zis.closeEntry();
210             }
211         }
212     }
213 
214     private void findXSLinClassesDir() {
215         String base = "/WEB-INF/classes/xsl/";
216         for (String path : diveInto(base)) {
217             if (path.endsWith(".xsl")) {
218                 foundStylesheet(path, base);
219             }
220         }
221     }
222 
223     private void foundStylesheet(String path, String source) {
224         String file = path.substring(path.lastIndexOf("xsl/") + 4);
225         LOGGER.info("Found {} in {}", file, source);
226         Stylesheet stylesheet = getStylesheet(file);
227         if (source.startsWith("/WEB-INF/")) {
228             source = source.substring("/WEB-INF/".length()); // cut off
229         }
230         stylesheet.origin.add(source);
231     }
232 
233     private Stylesheet getStylesheet(String name) {
234         return stylesheets.computeIfAbsent(name, Stylesheet::new);
235     }
236 
237     class Stylesheet {
238         String name;
239 
240         Set<String> origin = new HashSet<>();
241 
242         Set<String> includes = new HashSet<>();
243 
244         Set<String> imports = new HashSet<>();
245 
246         List<Element> templates = new ArrayList<>();
247 
248         Element xsl;
249 
250         Stylesheet(String name) {
251             this.name = name;
252         }
253 
254         void inspect() {
255             resolveXSL();
256             if (xsl != null) {
257                 listTemplates();
258                 findIncludes("include", includes);
259                 findIncludes("import", imports);
260             }
261         }
262 
263         private void resolveXSL() {
264             String uri = "resource:xsl/" + name;
265             resolveXSL(uri);
266             if (xsl == null) {
267                 resolveXSL(name);
268                 if (xsl != null) {
269                     origin.add("URIResolver");
270                 }
271             }
272         }
273 
274         private void resolveXSL(String uri) {
275             try {
276                 xsl = MCRURIResolver.instance().resolve(uri);
277             } catch (Exception ex) {
278                 String msg = "Exception resolving stylesheet " + name;
279                 LOGGER.warn(msg, ex);
280             }
281         }
282 
283         private void findIncludes(String tag, Set<String> set) {
284             List<Element> includes = xsl.getChildren(tag, MCRConstants.XSL_NAMESPACE);
285             for (Element include : includes) {
286                 String href = include.getAttributeValue("href");
287                 LOGGER.info("{} {}s {}", name, tag, href);
288                 set.add(href);
289                 if (!stylesheets.containsKey(href)) {
290                     unknown.add(href);
291                 }
292             }
293         }
294 
295         private void listTemplates() {
296             List<Element> list = xsl.getChildren("template", MCRConstants.XSL_NAMESPACE);
297             IteratorIterable<Element> callTemplateElements = xsl
298                 .getDescendants(Filters.element("call-template", MCRConstants.XSL_NAMESPACE));
299             LinkedList<Element> templates = new LinkedList<>(list);
300             HashSet<String> callNames = new HashSet<>();
301             for (Element callTemplate : callTemplateElements) {
302                 String name = callTemplate.getAttributeValue("name");
303                 if (callNames.add(name)) {
304                     templates.add(callTemplate);
305                 }
306             }
307             for (Element template : templates) {
308                 Element copy = template.clone();
309                 copy.removeContent();
310                 this.templates.add(copy);
311             }
312         }
313 
314         Element buildXML() {
315             Element elem = new Element("stylesheet");
316             elem.setAttribute("name", name);
317             addValues(elem, "origin", origin);
318             addValues(elem, "includes", includes);
319             addValues(elem, "imports", imports);
320 
321             for (Element template : templates) {
322                 elem.addContent(template.clone());
323             }
324 
325             return elem;
326         }
327 
328         private void addValues(Element parent, String tag, Set<String> set) {
329             for (String value : set) {
330                 parent.addContent(new Element(tag).setText(value));
331             }
332         }
333     }
334 }