View Javadoc
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 static java.util.Objects.requireNonNull;
19  
20  import java.io.StringWriter;
21  import java.io.Writer;
22  import java.util.UUID;
23  import java.util.function.Consumer;
24  import javax.annotation.Nonnull;
25  import javax.annotation.Nullable;
26  import org.apache.commons.io.IOUtils;
27  import org.apache.velocity.VelocityContext;
28  import org.apache.velocity.app.Velocity;
29  import org.apache.velocity.context.Context;
30  import org.apache.velocity.runtime.RuntimeConstants;
31  import org.apache.velocity.runtime.RuntimeSingleton;
32  import org.apache.velocity.tools.Scope;
33  import org.apache.velocity.tools.ToolManager;
34  import org.apache.velocity.tools.config.EasyFactoryConfiguration;
35  import org.apache.velocity.tools.generic.ClassTool;
36  import org.apache.velocity.tools.generic.ComparisonDateTool;
37  import org.apache.velocity.tools.generic.ContextTool;
38  import org.apache.velocity.tools.generic.DisplayTool;
39  import org.apache.velocity.tools.generic.EscapeTool;
40  import org.apache.velocity.tools.generic.FieldTool;
41  import org.apache.velocity.tools.generic.LinkTool;
42  import org.apache.velocity.tools.generic.LoopTool;
43  import org.apache.velocity.tools.generic.MathTool;
44  import org.apache.velocity.tools.generic.NumberTool;
45  import org.apache.velocity.tools.generic.RenderTool;
46  import org.apache.velocity.tools.generic.ResourceTool;
47  import org.apache.velocity.tools.generic.XmlTool;
48  import org.devacfr.maven.skins.reflow.HtmlTool;
49  import org.devacfr.maven.skins.reflow.ISkinConfig;
50  import org.devacfr.maven.skins.reflow.JsoupUtils;
51  import org.devacfr.maven.skins.reflow.URITool;
52  import org.devacfr.maven.skins.reflow.snippet.SnippetComponent.Type;
53  import org.jsoup.nodes.Element;
54  import org.jsoup.nodes.Node;
55  import org.jsoup.nodes.TextNode;
56  import org.slf4j.Logger;
57  import org.slf4j.LoggerFactory;
58  
59  /**
60   * @author Christophe Friederich
61   * @version 2.4
62   */
63  public class SnippetContext {
64  
65    /** */
66    private static final Logger LOGGER = LoggerFactory.getLogger(SnippetContext.class);
67  
68    /** **/
69    private String htmlSource;
70  
71    /** */
72    private ISkinConfig config;
73  
74    /** */
75    private final SnippetParser parser;
76  
77    /**
78     * Constructor.
79     *
80     * @param parser
81     */
82    public SnippetContext(@Nonnull final SnippetParser parser) {
83      this.parser = requireNonNull(parser);
84    }
85  
86    /**
87     * Reset the context.
88     */
89    public void reset() {
90      this.htmlSource = null;
91      this.config = null;
92    }
93  
94    /**
95     * Generate a unique snippet identifier.
96     *
97     * @return a unique snippet identifier
98     */
99    public String generateSnippetIdentifier() {
100     return "snippet-placement-" + UUID.randomUUID().toString();
101   }
102 
103   /**
104    * Returns the current parser.
105    *
106    * @return the current parser
107    */
108   public SnippetParser getParser() {
109     return this.parser;
110   }
111 
112   /**
113    * Returns the skin configuration.
114    *
115    * @return the skin configuration
116    */
117   public ISkinConfig getConfig() {
118     return config;
119   }
120 
121   /**
122    * Create a child parser.
123    *
124    * @return a new SnippetParser
125    */
126   public SnippetParser createChildParser() {
127     final SnippetParser parser = new SnippetParser();
128     return parser;
129   }
130 
131   /**
132    * Sets the skin configuration.
133    *
134    * @param config
135    *          the skin configuration
136    */
137   public void setConfig(final ISkinConfig config) {
138     this.config = config;
139   }
140 
141   /**
142    * Sets the html source.
143    *
144    * @param htmlSource
145    *          the html source
146    */
147   void setHtmlSource(final String htmlSource) {
148     this.htmlSource = htmlSource;
149   }
150 
151   /**
152    * Returns the html source.
153    *
154    * @return the html source
155    */
156   public String html() {
157     return htmlSource;
158   }
159 
160   /**
161    * Returns the html document.
162    *
163    * @return the html document
164    */
165   public Element document() {
166     return JsoupUtils.createHtmlDocument(html());
167   }
168 
169   /**
170    * Create a component from element and tokens.
171    *
172    * @param element
173    *          the html element to use.
174    * @param startToken
175    *          the start token
176    * @param endToken
177    *          the end token.
178    * @return Returns a new {@link Component} representing the snippet.
179    */
180   @Nonnull
181   public Component<?> create(@Nonnull final Element element,
182     @Nonnull final ComponentToken startToken,
183     @Nullable final ComponentToken endToken) {
184     requireNonNull(element);
185     requireNonNull(startToken);
186     Component<?> component = null;
187     if (parser.isSnippet(startToken.name())) {
188       // create snippet component
189       component = SnippetComponent.createSnippet(element, null, startToken.type());
190       recurciveCreateComponent(element, component);
191     } else {
192       // create generic component
193       component = Component.createComponent(element, null);
194       recurciveCreateComponent(element, component);
195     }
196     return component;
197   }
198 
199   /**
200    * Create a component from element and component.
201    *
202    * @param element
203    *          the html element to use.
204    * @param parent
205    *          the parent component.
206    * @return Returns a new {@link SnippetComponent} representing the snippet.
207    */
208   @Nonnull
209   public Component<?> create(@Nonnull final Element element, final Component<?> parent) {
210     requireNonNull(element);
211     Component<?> component = null;
212     if (parser.isSnippet(element.tagName())) {
213       component = SnippetComponent.createSnippet(element, parent, Type.webComponent);
214     } else {
215       component = Component.createComponent(element, parent);
216     }
217     recurciveCreateComponent(element, component);
218     return component;
219   }
220 
221   /**
222    * Recursively create components from element.
223    *
224    * @param element
225    *          the html element to use.
226    * @param parent
227    *          the parent component.
228    */
229   private void recurciveCreateComponent(@Nonnull final Node element, final Component<?> parent) {
230     element.childNodes().forEach(child -> {
231       Component<?> component = null;
232       // accept textnode not empty as component.
233       if (child instanceof TextNode && ((TextNode) child).text().trim().length() > 0) {
234         component = Component.createComponent(child, parent);
235       } else if ("p".equals(child.nodeName()) && child.outerHtml().length() == 7) {
236         // skip empty <p> tags
237       } else if (child instanceof Element) {
238         final Element el = (Element) child;
239         component = create(el, parent);
240       }
241       if (component != null) {
242         parent.addChild(component);
243       }
244     });
245   }
246 
247   /**
248    * Render the component.
249    *
250    * @param component
251    *          the component to render.
252    */
253   protected void render(final Component<?> component) {
254     traverseTee(component, c -> {
255       if (c instanceof SnippetComponent) {
256         ((SnippetComponent<?>) c).render(this);
257       }
258     });
259     if (component instanceof SnippetComponent) {
260       ((SnippetComponent<?>) component).render(this);
261     }
262   }
263 
264   private void traverseTee(final Component<?> component, final Consumer<Component<?>> consumer) {
265     final Consumer<Component<?>> traverse = c -> traverseTee(c, consumer);
266     component.getChildren().forEach(consumer.andThen(traverse));
267   }
268 
269   protected String renderComponent(final SnippetComponent<?> component, final Context context) {
270     final StringWriter writer = new StringWriter();
271     try {
272       mergeTemplate(component, context, writer);
273       return writer.toString();
274     } finally {
275       IOUtils.closeQuietly(writer);
276     }
277   }
278 
279   /**
280    * Merges the template for the given component.
281    *
282    * @param component
283    *          the snippet component
284    * @param contextParent
285    *          the velocity context parent
286    * @param writer
287    *          the writer to use.
288    */
289   protected void mergeTemplate(final SnippetComponent<?> component, final Context contextParent, final Writer writer) {
290     boolean found = false;
291     for (final String path : this.parser.getSnippetPaths()) {
292       final String filePath = path + '/' + component.getName() + ".vm";
293       if (Velocity.resourceExists(filePath)) {
294         found = true;
295         final Context context = createVelocityContext(contextParent);
296         context.put("snippet", component);
297         context.put("snippetPath", filePath);
298         context.put("config", this.config);
299         if (this.config.getContext() != null) {
300           context.put("pageContext", this.config.getContext());
301           context.put("pageType", this.config.getContext().getType());
302         }
303         context.put("velocity", Velocity.class);
304         context.put("site", this.config.getSiteModel());
305 
306         Velocity.mergeTemplate("META-INF/skin/snippets/_snippet.vm",
307           RuntimeSingleton.getString(RuntimeConstants.INPUT_ENCODING, RuntimeConstants.ENCODING_DEFAULT),
308           context,
309           writer);
310         break;
311       } else {
312         if (LOGGER.isDebugEnabled()) {
313           LOGGER.debug("Template for component '{}' not found in path:{} ", component, filePath);
314         }
315       }
316     }
317     if (!found) {
318       LOGGER.warn("The snippet '{}' template doesn't exist", component.getName());
319     }
320   }
321 
322   /**
323    * Creates a Velocity Context with all generic tools configured wit the site rendering context.
324    *
325    * @param contextParent
326    *          velocity context parent
327    * @return a Velocity tools managed context
328    */
329   protected Context createVelocityContext(final Context contextParent) {
330     final VelocityContext context = new VelocityContext(contextParent);
331     return context;
332   }
333 
334   /**
335    * Creates a ToolManager with all generic tools configured.
336    *
337    * @return a Velocity tools managed
338    */
339   protected static ToolManager createToolManaged() {
340 
341     final EasyFactoryConfiguration config = new EasyFactoryConfiguration(false);
342     config.property("safeMode", Boolean.FALSE);
343     config.toolbox(Scope.REQUEST)
344         .tool(ContextTool.class)
345         .tool(LinkTool.class)
346         .tool(LoopTool.class)
347         .tool(RenderTool.class);
348     config.toolbox(Scope.APPLICATION)
349         .tool(ClassTool.class)
350         .tool(ComparisonDateTool.class)
351         .tool(DisplayTool.class)
352         .tool(EscapeTool.class)
353         .tool(FieldTool.class)
354         .tool(MathTool.class)
355         .tool(NumberTool.class)
356         .tool(ResourceTool.class)
357         .tool(XmlTool.class)
358         .tool(URITool.class)
359         .tool(HtmlTool.class);
360 
361     final ToolManager manager = new ToolManager(false, false);
362     manager.configure(config);
363     return manager;
364   }
365 
366   /**
367    * Represents a snippet resource.
368    */
369   public static class SnippetResource {
370 
371     /** */
372     private final String name;
373 
374     /** */
375     private final String path;
376 
377     /**
378      * Constructor.
379      *
380      * @param name
381      *          the resource name
382      * @param path
383      *          the resource path
384      */
385     public SnippetResource(final String name, final String path) {
386       this.name = name;
387       this.path = path;
388     }
389 
390     /** */
391     public String getName() {
392       return name;
393     }
394 
395     /** */
396     public String getPath() {
397       return path;
398     }
399 
400     /**
401      * {@inheritDoc}
402      */
403     @Override
404     public String toString() {
405       return "SnippetResource [name=" + name + ", path=" + path + "]";
406     }
407   }
408 
409 }