1. Project Clover database mer. févr. 4 2026 12:48:28 CET
  2. Package org.devacfr.maven.skins.reflow.snippet

File SnippetParser.java

 

Coverage histogram

../../../../../../img/srcFileCovDistChart8.png
45% of files have more coverage

Code metrics

54
124
22
1
430
258
55
0,44
5,64
22
2,5

Classes

Class Line # Actions
SnippetParser 58 124 0% 55 56
0.7272%
 

Contributing tests

This file is covered by 66 tests. .

Source view

1    /*
2    * Copyright 2012-2025 Christophe Friederich
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10    * Unless required by applicable law or agreed to in writing, software
11    * distributed under the License is distributed on an "AS IS" BASIS,
12    * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13    * See the License for the specific language governing permissions and
14    * limitations under the License.
15    */
16    package org.devacfr.maven.skins.reflow.snippet;
17   
18    import com.google.common.collect.Lists;
19    import com.google.common.collect.Maps;
20    import java.io.File;
21    import java.io.IOException;
22    import java.net.URL;
23    import java.net.URLDecoder;
24    import java.nio.charset.StandardCharsets;
25    import java.nio.file.Path;
26    import java.nio.file.Paths;
27    import java.util.ArrayList;
28    import java.util.Arrays;
29    import java.util.Collections;
30    import java.util.Enumeration;
31    import java.util.Iterator;
32    import java.util.List;
33    import java.util.Map;
34    import java.util.jar.JarEntry;
35    import java.util.jar.JarFile;
36    import java.util.stream.Collectors;
37    import javax.annotation.Nonnull;
38    import javax.annotation.Nullable;
39    import org.apache.commons.io.FilenameUtils;
40    import org.apache.velocity.context.Context;
41    import org.codehaus.plexus.util.FileUtils;
42    import org.devacfr.maven.skins.reflow.ISkinConfig;
43    import org.devacfr.maven.skins.reflow.JsoupUtils;
44    import org.devacfr.maven.skins.reflow.snippet.Processor.WebComponentProcessor;
45    import org.devacfr.maven.skins.reflow.snippet.SnippetContext.SnippetResource;
46    import org.jsoup.nodes.Element;
47    import org.jsoup.nodes.Node;
48    import org.jsoup.select.Collector;
49    import org.jsoup.select.Elements;
50    import org.jsoup.select.QueryParser;
51    import org.slf4j.Logger;
52    import org.slf4j.LoggerFactory;
53   
54    /**
55    * @author Christophe Friederich
56    * @version 2.4
57    */
 
58    public class SnippetParser {
59   
60    /** */
61    private static final Logger LOGGER = LoggerFactory.getLogger(SnippetParser.class);
62   
63    /** */
64    private static final List<String> DEFAULT_PATHS = Lists.newArrayList("src/site/layouts/snippets",
65    "META-INF/skin/snippets");
66   
67    /** */
68    private final ComponentResolver resolver;
69   
70    /** */
71    private final ArrayList<ComponentToken> stack;
72   
73    /** */
74    private Iterator<Element> it;
75   
76    /** */
77    private Processor state = null;
78   
79    /** */
80    private final Processor processor;
81   
82    /** */
83    private final SnippetContext snippetContext;
84   
85    /** */
86    private ComponentToken currentToken;
87   
88    /** */
89    private final List<String> snippetPaths = Lists.newArrayList(DEFAULT_PATHS);
90   
91    /** */
92    private final Map<String, SnippetResource> snippetResources = Maps.newHashMap();
93   
94    /**
95    * Constructor.
96    */
 
97  68 toggle public SnippetParser() {
98  68 stack = Lists.newArrayListWithCapacity(32);
99  68 snippetContext = new SnippetContext(this);
100  68 resolver = new ComponentResolver(this);
101  68 processor = new WebComponentProcessor(this);
102  68 snippetResources.putAll(loadSnippetResources(getSnippetPaths()));
103  68 if (LOGGER.isTraceEnabled()) {
104  0 LOGGER.trace("Snippet Paths: {}", getSnippetPaths());
105  0 LOGGER.trace("Loaded {} snippet resources: {}", snippetResources.size(), getSnippets());
106    }
107    }
108   
109    /**
110    * @return Returns the snippet resources.
111    */
 
112  0 toggle @Nonnull
113    protected Map<String, SnippetResource> getSnippetResources() {
114  0 return Collections.unmodifiableMap(snippetResources);
115    }
116   
117    /**
118    * @return Returns the snippet names.
119    */
 
120  1818 toggle @Nonnull
121    protected List<String> getSnippets() {
122  1818 return snippetResources.keySet().stream().collect(Collectors.toList());
123    }
124   
125    /**
126    * @return Returns the snippet resource paths.
127    */
 
128  210 toggle @Nonnull
129    public List<String> getSnippetPaths() {
130  210 return Collections.unmodifiableList(snippetPaths);
131    }
132   
133    /**
134    * Insert a resource path at the given index.
135    *
136    * @param path
137    * the resource path to add
138    * @param index
139    * the index where to insert the path
140    * @return Returns this parser.
141    */
 
142  34 toggle public SnippetParser insertResourcePath(final int index, final @Nonnull String path) {
143  34 this.snippetPaths.add(index, path);
144  34 this.refreshParser();
145  34 return this;
146    }
147   
148    /**
149    * Add a resource path.
150    *
151    * @param path
152    * the resource path to add
153    * @return Returns this parser.
154    */
 
155  2 toggle public SnippetParser addResourcePath(final @Nonnull String... path) {
156  2 this.snippetPaths.addAll(Arrays.asList(path));
157  2 this.refreshParser();
158  2 return this;
159    }
160   
161    /**
162    * Refresh the parser by reloading snippet resources.
163    *
164    * @return Returns this parser.
165    */
 
166  36 toggle public SnippetParser refreshParser() {
167  36 snippetResources.clear();
168  36 snippetResources.putAll(loadSnippetResources(getSnippetPaths()));
169  36 if (LOGGER.isTraceEnabled()) {
170  0 LOGGER.trace("Reloaded {} snippet resources: {}", snippetResources.size(), getSnippets());
171    }
172  36 return this;
173    }
174   
175    /**
176    * @param node
177    * the Jsoup node to use
178    * @return Returns {@code true} if the node is a snippet component.
179    */
 
180  1190 toggle public boolean isSnippet(final Node node) {
181  1190 return getSnippets().stream().anyMatch(snippet -> snippet.equalsIgnoreCase(JsoupUtils.getNodeName(node)));
182    }
183   
184    /**
185    * @param nodeName
186    * the Jsoup node name to use
187    * @return Returns {@code true} if the node name is a snippet component.
188    */
 
189  520 toggle public boolean isSnippet(final String nodeName) {
190  520 return getSnippets().stream().anyMatch(snippet -> snippet.equalsIgnoreCase(nodeName));
191    }
192   
193    /**
194    * Check if the document contains at least one snippet component.
195    *
196    * @param document
197    * the Jsoup element to use
198    * @return Returns {@code true} if the document contains at least one snippet component.
199    */
 
200  108 toggle public boolean hasIncludedSnippetComponent(final Element document) {
201  108 final String tags = getSnippets().stream().collect(Collectors.joining(","));
202  108 if (LOGGER.isDebugEnabled()) {
203  108 LOGGER.debug("Check document for snippet components: {}", tags);
204    }
205  108 return Collector.findFirst(QueryParser.parse(tags), document) != null;
206    }
207   
208    /**
209    * Parse the given HTML source.
210    *
211    * @param config
212    * the skin config
213    * @param htmlSource
214    * the HTML source to parse
215    * @return Returns the snippet context.
216    * @throws IOException
217    * if an I/O error occurs.
218    */
 
219  34 toggle public SnippetContext parse(final ISkinConfig config, @Nullable String htmlSource) throws IOException {
220  34 if (htmlSource == null) {
221  0 htmlSource = "";
222    }
223   
224  34 snippetContext.reset();
225  34 snippetContext.setConfig(config);
226  34 snippetContext.setHtmlSource(htmlSource);
227   
228  34 if (LOGGER.isDebugEnabled()) {
229  34 LOGGER.debug("Parse Snippet");
230  34 LOGGER.debug(htmlSource);
231    }
232   
233    // find all snippets
234  34 final Element doc = resolver.normalize(JsoupUtils.createHtmlDocument(htmlSource));
235   
236  34 if (LOGGER.isDebugEnabled()) {
237  34 LOGGER.debug("Normalized HTML source:");
238  34 LOGGER.debug(doc.html());
239    }
240   
241  34 final Elements elements = resolver.collect(doc);
242   
243  136 for (it = elements.iterator(); it.hasNext();) {
244  102 try {
245  102 parse();
246    } catch (final Exception ex) {
247  0 throw new SnippetParseException(
248    "error on parse token " + currentToken + " when generate file " + config.getFileId(), ex);
249    }
250    }
251  34 snippetContext.setHtmlSource(doc.html());
252  34 return snippetContext;
253    }
254   
255    /**
256    * Parse the current element.
257    */
 
258  182 toggle protected void parse() {
259  182 if (!it.hasNext()) {
260  0 throw new SnippetParseException("EOF");
261    }
262  182 final Element element = it.next();
263  182 currentToken = resolver.create(element);
264  182 if (currentToken == null) {
265  0 throw new SnippetParseException("unknown component: " + element);
266    }
267  182 state = processor;
268  182 parse(currentToken);
269  182 currentToken = null;
270    }
271   
 
272  182 toggle protected void parse(final ComponentToken token) {
273  182 state.parse(token);
274    }
275   
 
276  0 toggle protected ComponentToken currentToken() {
277  0 final int size = stack.size();
278  0 return size > 0 ? stack.get(size - 1) : null;
279    }
280   
 
281  80 toggle protected ComponentToken pop() {
282  80 final int size = stack.size();
283  80 if (LOGGER.isDebugEnabled()) {
284  80 LOGGER.debug("Stack size befor pop: {}", size);
285    }
286   
287  80 if (size == 0) {
288  0 throw new SnippetParseException("Cannot pop from empty stack");
289    }
290  80 try {
291   
292  80 final ComponentToken element = stack.remove(size - 1);
293  80 if (LOGGER.isDebugEnabled()) {
294  80 LOGGER.debug("Remove component from stack: {}", element);
295    }
296  80 return element;
297    } catch (final Exception e) {
298  0 throw new SnippetParseException("Error while popping from stack", e);
299    }
300    }
301   
302    /**
303    * Push a component token onto the stack.
304    *
305    * @param token
306    * the component token to push
307    */
 
308  80 toggle protected void push(final ComponentToken token) {
309  80 stack.add(token);
310  80 if (LOGGER.isDebugEnabled()) {
311  80 LOGGER.debug("Add component to stack: {}", token);
312    }
313    }
314   
315    /**
316    * @return Returns the snippet context.
317    */
 
318  104 toggle public SnippetContext getSnippetContext() {
319  104 return snippetContext;
320    }
321   
 
322  0 toggle public Context getVelocityContext() {
323  0 return this.snippetContext.getConfig().getVelocityContext();
324    }
325   
 
326  104 toggle private Map<String, SnippetResource> loadSnippetResources(final List<String> snippetPaths) {
327  104 final Map<String, SnippetResource> resources = Maps.newHashMap();
328  104 final List<String> paths = Lists.newArrayList(snippetPaths);
329  104 Collections.reverse(paths);
330  104 for (final String path : paths) {
331  246 try {
332  246 List<String> files = getResources(path);
333  246 if (files.isEmpty()) {
334  104 files = FileUtils.getFiles(Path.of(path).toFile(), "*", null)
335    .stream()
336    .map(File::getAbsolutePath)
337    .collect(Collectors.toList());
338    }
339  142 for (final String file : files) {
340  4776 final SnippetResource resource = new SnippetResource(file, path + '/' + file);
341  4776 final String name = FilenameUtils.getBaseName(file);
342  4776 final String ext = FilenameUtils.getExtension(file);
343  4776 if (!ext.equalsIgnoreCase("vm")) {
344    // if (LOGGER.isDebugEnabled()) {
345    // LOGGER.debug("Ignore snippet resource with unsupported extension: {}
346    // (allowed: vm)", file);
347    // }
348  2166 continue;
349    }
350  2610 if (!name.startsWith("_")) {
351  2506 resources.put(name, resource);
352    }
353    }
354    } catch (final Exception e) {
355  104 LOGGER.warn("Cannot load snippet resources from path: {}", path, e);
356    }
357    }
358  104 return resources;
359    }
360   
361    /**
362    * Get resource as stream.
363    *
364    * @param resource
365    * the resource
366    * @return the resource as stream
367    */
 
368  246 toggle private List<String> getResources(final String path) throws Exception {
369  246 final List<String> filenames = Lists.newArrayList();
370   
371  246 final URL url = getResource(path);
372  246 if (LOGGER.isTraceEnabled()) {
373  0 LOGGER.trace("Loading resources from path: {} (url={})", path, url);
374    }
375  246 if (url != null) {
376  142 if (url.getProtocol().equals("file")) {
377  142 final File file = Paths.get(url.toURI()).toFile();
378  142 if (file != null) {
379  142 final File[] files = file.listFiles();
380  142 if (files != null) {
381  142 for (final File filename : files) {
382  4776 filenames.add(filename.toString());
383    }
384    }
385    }
386  0 } else if (url.getProtocol().equals("jar")) {
387  0 final String dirname = path + "/";
388  0 final String p = url.getPath();
389  0 final String jarPath = p.substring(5, p.indexOf("!"));
390  0 try (JarFile jar = new JarFile(URLDecoder.decode(jarPath, StandardCharsets.UTF_8.name()))) {
391  0 final Enumeration<JarEntry> entries = jar.entries();
392  0 while (entries.hasMoreElements()) {
393  0 final JarEntry entry = entries.nextElement();
394  0 final String name = entry.getName();
395  0 if (name.startsWith(dirname) && !dirname.equals(name)) {
396  0 final URL resource = getResource(name);
397  0 filenames.add(resource.toString());
398    }
399    }
400    }
401    }
402    }
403  246 if (LOGGER.isTraceEnabled()) {
404  0 LOGGER.trace("Found {} resources ({}) in path: {}", filenames.size(), filenames, path);
405    }
406  246 return filenames;
407    }
408   
409    /**
410    * Get resource.
411    *
412    * @param resource
413    * the resource
414    * @return the resource
415    */
 
416  246 toggle private URL getResource(final String resource) {
417  246 final URL url = getContextClassLoader().getResource(resource);
418  246 return url == null ? this.getClass().getClassLoader().getResource(resource) : url;
419    }
420   
421    /**
422    * Get the context class loader.
423    *
424    * @return the context class loader
425    */
 
426  246 toggle private static ClassLoader getContextClassLoader() {
427  246 return Thread.currentThread().getContextClassLoader();
428    }
429   
430    }