1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
56
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
96
97 public SnippetParser() {
98 stack = Lists.newArrayListWithCapacity(32);
99 snippetContext = new SnippetContext(this);
100 resolver = new ComponentResolver(this);
101 processor = new WebComponentProcessor(this);
102 snippetResources.putAll(loadSnippetResources(getSnippetPaths()));
103 if (LOGGER.isTraceEnabled()) {
104 LOGGER.trace("Snippet Paths: {}", getSnippetPaths());
105 LOGGER.trace("Loaded {} snippet resources: {}", snippetResources.size(), getSnippets());
106 }
107 }
108
109
110
111
112 @Nonnull
113 protected Map<String, SnippetResource> getSnippetResources() {
114 return Collections.unmodifiableMap(snippetResources);
115 }
116
117
118
119
120 @Nonnull
121 protected List<String> getSnippets() {
122 return snippetResources.keySet().stream().collect(Collectors.toList());
123 }
124
125
126
127
128 @Nonnull
129 public List<String> getSnippetPaths() {
130 return Collections.unmodifiableList(snippetPaths);
131 }
132
133
134
135
136
137
138
139
140
141
142 public SnippetParser insertResourcePath(final int index, final @Nonnull String path) {
143 this.snippetPaths.add(index, path);
144 this.refreshParser();
145 return this;
146 }
147
148
149
150
151
152
153
154
155 public SnippetParser addResourcePath(final @Nonnull String... path) {
156 this.snippetPaths.addAll(Arrays.asList(path));
157 this.refreshParser();
158 return this;
159 }
160
161
162
163
164
165
166 public SnippetParser refreshParser() {
167 snippetResources.clear();
168 snippetResources.putAll(loadSnippetResources(getSnippetPaths()));
169 if (LOGGER.isTraceEnabled()) {
170 LOGGER.trace("Reloaded {} snippet resources: {}", snippetResources.size(), getSnippets());
171 }
172 return this;
173 }
174
175
176
177
178
179
180 public boolean isSnippet(final Node node) {
181 return getSnippets().stream().anyMatch(snippet -> snippet.equalsIgnoreCase(JsoupUtils.getNodeName(node)));
182 }
183
184
185
186
187
188
189 public boolean isSnippet(final String nodeName) {
190 return getSnippets().stream().anyMatch(snippet -> snippet.equalsIgnoreCase(nodeName));
191 }
192
193
194
195
196
197
198
199
200 public boolean hasIncludedSnippetComponent(final Element document) {
201 final String tags = getSnippets().stream().collect(Collectors.joining(","));
202 if (LOGGER.isDebugEnabled()) {
203 LOGGER.debug("Check document for snippet components: {}", tags);
204 }
205 return Collector.findFirst(QueryParser.parse(tags), document) != null;
206 }
207
208
209
210
211
212
213
214
215
216
217
218
219 public SnippetContext parse(final ISkinConfig config, @Nullable String htmlSource) throws IOException {
220 if (htmlSource == null) {
221 htmlSource = "";
222 }
223
224 snippetContext.reset();
225 snippetContext.setConfig(config);
226 snippetContext.setHtmlSource(htmlSource);
227
228 if (LOGGER.isDebugEnabled()) {
229 LOGGER.debug("Parse Snippet");
230 LOGGER.debug(htmlSource);
231 }
232
233
234 final Element doc = resolver.normalize(JsoupUtils.createHtmlDocument(htmlSource));
235
236 if (LOGGER.isDebugEnabled()) {
237 LOGGER.debug("Normalized HTML source:");
238 LOGGER.debug(doc.html());
239 }
240
241 final Elements elements = resolver.collect(doc);
242
243 for (it = elements.iterator(); it.hasNext();) {
244 try {
245 parse();
246 } catch (final Exception ex) {
247 throw new SnippetParseException(
248 "error on parse token " + currentToken + " when generate file " + config.getFileId(), ex);
249 }
250 }
251 snippetContext.setHtmlSource(doc.html());
252 return snippetContext;
253 }
254
255
256
257
258 protected void parse() {
259 if (!it.hasNext()) {
260 throw new SnippetParseException("EOF");
261 }
262 final Element element = it.next();
263 currentToken = resolver.create(element);
264 if (currentToken == null) {
265 throw new SnippetParseException("unknown component: " + element);
266 }
267 state = processor;
268 parse(currentToken);
269 currentToken = null;
270 }
271
272 protected void parse(final ComponentToken token) {
273 state.parse(token);
274 }
275
276 protected ComponentToken currentToken() {
277 final int size = stack.size();
278 return size > 0 ? stack.get(size - 1) : null;
279 }
280
281 protected ComponentToken pop() {
282 final int size = stack.size();
283 if (LOGGER.isDebugEnabled()) {
284 LOGGER.debug("Stack size befor pop: {}", size);
285 }
286
287 if (size == 0) {
288 throw new SnippetParseException("Cannot pop from empty stack");
289 }
290 try {
291
292 final ComponentToken element = stack.remove(size - 1);
293 if (LOGGER.isDebugEnabled()) {
294 LOGGER.debug("Remove component from stack: {}", element);
295 }
296 return element;
297 } catch (final Exception e) {
298 throw new SnippetParseException("Error while popping from stack", e);
299 }
300 }
301
302
303
304
305
306
307
308 protected void push(final ComponentToken token) {
309 stack.add(token);
310 if (LOGGER.isDebugEnabled()) {
311 LOGGER.debug("Add component to stack: {}", token);
312 }
313 }
314
315
316
317
318 public SnippetContext getSnippetContext() {
319 return snippetContext;
320 }
321
322 public Context getVelocityContext() {
323 return this.snippetContext.getConfig().getVelocityContext();
324 }
325
326 private Map<String, SnippetResource> loadSnippetResources(final List<String> snippetPaths) {
327 final Map<String, SnippetResource> resources = Maps.newHashMap();
328 final List<String> paths = Lists.newArrayList(snippetPaths);
329 Collections.reverse(paths);
330 for (final String path : paths) {
331 try {
332 List<String> files = getResources(path);
333 if (files.isEmpty()) {
334 files = FileUtils.getFiles(Path.of(path).toFile(), "*", null)
335 .stream()
336 .map(File::getAbsolutePath)
337 .collect(Collectors.toList());
338 }
339 for (final String file : files) {
340 final SnippetResource resource = new SnippetResource(file, path + '/' + file);
341 final String name = FilenameUtils.getBaseName(file);
342 final String ext = FilenameUtils.getExtension(file);
343 if (!ext.equalsIgnoreCase("vm")) {
344
345
346
347
348 continue;
349 }
350 if (!name.startsWith("_")) {
351 resources.put(name, resource);
352 }
353 }
354 } catch (final Exception e) {
355 LOGGER.warn("Cannot load snippet resources from path: {}", path, e);
356 }
357 }
358 return resources;
359 }
360
361
362
363
364
365
366
367
368 private List<String> getResources(final String path) throws Exception {
369 final List<String> filenames = Lists.newArrayList();
370
371 final URL url = getResource(path);
372 if (LOGGER.isTraceEnabled()) {
373 LOGGER.trace("Loading resources from path: {} (url={})", path, url);
374 }
375 if (url != null) {
376 if (url.getProtocol().equals("file")) {
377 final File file = Paths.get(url.toURI()).toFile();
378 if (file != null) {
379 final File[] files = file.listFiles();
380 if (files != null) {
381 for (final File filename : files) {
382 filenames.add(filename.toString());
383 }
384 }
385 }
386 } else if (url.getProtocol().equals("jar")) {
387 final String dirname = path + "/";
388 final String p = url.getPath();
389 final String jarPath = p.substring(5, p.indexOf("!"));
390 try (JarFile jar = new JarFile(URLDecoder.decode(jarPath, StandardCharsets.UTF_8.name()))) {
391 final Enumeration<JarEntry> entries = jar.entries();
392 while (entries.hasMoreElements()) {
393 final JarEntry entry = entries.nextElement();
394 final String name = entry.getName();
395 if (name.startsWith(dirname) && !dirname.equals(name)) {
396 final URL resource = getResource(name);
397 filenames.add(resource.toString());
398 }
399 }
400 }
401 }
402 }
403 if (LOGGER.isTraceEnabled()) {
404 LOGGER.trace("Found {} resources ({}) in path: {}", filenames.size(), filenames, path);
405 }
406 return filenames;
407 }
408
409
410
411
412
413
414
415
416 private URL getResource(final String resource) {
417 final URL url = getContextClassLoader().getResource(resource);
418 return url == null ? this.getClass().getClassLoader().getResource(resource) : url;
419 }
420
421
422
423
424
425
426 private static ClassLoader getContextClassLoader() {
427 return Thread.currentThread().getContextClassLoader();
428 }
429
430 }