View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.devacfr.maven.skins.reflow.snippet;
20  
21  import javax.annotation.Nonnull;
22  import javax.annotation.Nullable;
23  
24  import java.io.StringWriter;
25  import java.io.Writer;
26  import java.lang.ref.WeakReference;
27  import java.util.List;
28  import java.util.UUID;
29  import java.util.function.Consumer;
30  
31  import com.google.common.collect.Lists;
32  import org.apache.velocity.app.Velocity;
33  import org.apache.velocity.context.Context;
34  import org.apache.velocity.runtime.RuntimeConstants;
35  import org.apache.velocity.runtime.RuntimeSingleton;
36  import org.apache.velocity.tools.Scope;
37  import org.apache.velocity.tools.ToolManager;
38  import org.apache.velocity.tools.config.EasyFactoryConfiguration;
39  import org.apache.velocity.tools.generic.ClassTool;
40  import org.apache.velocity.tools.generic.ComparisonDateTool;
41  import org.apache.velocity.tools.generic.ContextTool;
42  import org.apache.velocity.tools.generic.DisplayTool;
43  import org.apache.velocity.tools.generic.EscapeTool;
44  import org.apache.velocity.tools.generic.FieldTool;
45  import org.apache.velocity.tools.generic.LinkTool;
46  import org.apache.velocity.tools.generic.LoopTool;
47  import org.apache.velocity.tools.generic.MathTool;
48  import org.apache.velocity.tools.generic.NumberTool;
49  import org.apache.velocity.tools.generic.RenderTool;
50  import org.apache.velocity.tools.generic.ResourceTool;
51  import org.apache.velocity.tools.generic.XmlTool;
52  import org.devacfr.maven.skins.reflow.HtmlTool;
53  import org.devacfr.maven.skins.reflow.ISkinConfig;
54  import org.devacfr.maven.skins.reflow.URITool;
55  import org.devacfr.maven.skins.reflow.snippet.ComponentToken.Type;
56  import org.jsoup.Jsoup;
57  import org.jsoup.nodes.Document;
58  import org.jsoup.nodes.Element;
59  import org.jsoup.nodes.Node;
60  import org.jsoup.nodes.TextNode;
61  import org.slf4j.Logger;
62  import org.slf4j.LoggerFactory;
63  
64  import static java.util.Objects.requireNonNull;
65  
66  /**
67   * @author Christophe Friederich
68   * @version 2.4
69   */
70  public class SnippetContext {
71  
72      /** */
73      private static final Logger LOGGER = LoggerFactory.getLogger(SnippetContext.class);
74  
75      /** */
76      private static final List<String> DEFAULT_PATHS = Lists.newArrayList("src/site/layouts/snippets",
77          "META-INF/skin/snippets");
78  
79      /** */
80      private final List<SnippetComponent<?>> components = Lists.newArrayList();
81  
82      /** **/
83      private String htmlSource;
84  
85      /** */
86      private final List<String> snippetPaths = Lists.newArrayList(DEFAULT_PATHS);
87  
88      /** */
89      private ISkinConfig config;
90  
91      /** */
92      private final ToolManager toolManager;
93  
94      /** */
95      private final WeakReference<SnippetParser> parser;
96  
97      public SnippetContext(final SnippetParser parser) {
98          this.toolManager = createToolManaged();
99          this.parser = new WeakReference<>(parser);
100     }
101 
102     public void reset() {
103         this.htmlSource = null;
104         this.components.clear();
105         this.config = null;
106     }
107 
108     public String generateSnippetIdentifier() {
109         return "snippet-placement-" + UUID.randomUUID().toString();
110     }
111 
112     public List<SnippetComponent<?>> getComponents() {
113         return components;
114     }
115 
116     public SnippetParser getParser() {
117         return this.parser.get();
118     }
119 
120     public ISkinConfig getConfig() {
121         return config;
122     }
123 
124     public SnippetParser createChildParser() {
125         final SnippetParser parser = new SnippetParser();
126         getSnippetPaths().forEach(path -> parser.addResourcePath(path));
127         return parser;
128     }
129 
130     void setConfig(final ISkinConfig config) {
131         this.config = config;
132     }
133 
134     public List<String> getSnippetPaths() {
135         return snippetPaths;
136     }
137 
138     void addComponent(final SnippetComponent<?> component) {
139         this.components.add(component);
140     }
141 
142     void insertResourcePath(final String path, final int index) {
143         this.snippetPaths.add(index, path);
144     }
145 
146     void addResourcePath(final String path) {
147         this.snippetPaths.add(path);
148     }
149 
150     void setHtmlSource(final String htmlSource) {
151         this.htmlSource = htmlSource;
152     }
153 
154     public String html() {
155         return htmlSource;
156     }
157 
158     public Document document() {
159         return Jsoup.parse(html());
160     }
161 
162     @Nonnull
163     SnippetComponent<?> create(@Nonnull final Element element,
164         @Nonnull final ComponentToken startToken,
165         @Nullable final ComponentToken endToken) {
166         requireNonNull(element);
167         requireNonNull(startToken);
168         final SnippetComponent<?> component = SnippetComponent.createSnippet(element, null, startToken.type());
169         addComponent(component);
170         recurciveCreateComponent(element, component);
171         return component;
172     }
173 
174     @Nonnull
175     private SnippetComponent<?> create(@Nonnull final Element element, final Component<?> commponent) {
176         requireNonNull(element);
177         Type type = null;
178         if (element.hasAttr("shortcode")) {
179             type = Type.shortcode;
180         } else if (element.hasAttr("webcomponent")) {
181             type = Type.webComponent;
182         } else {
183             throw new SnippetParseException("Unknown snippet element");
184         }
185         final SnippetComponent<?> component = SnippetComponent.createSnippet(element, commponent, type);
186         addComponent(component);
187         recurciveCreateComponent(element, component);
188         return component;
189     }
190 
191     private void recurciveCreateComponent(@Nonnull final Node element, final Component<?> parent) {
192         element.childNodes().forEach(child -> {
193             Component<?> component = null;
194             // accept textnode not empty as component.
195             if (child instanceof TextNode && ((TextNode) child).text().length() > 1) {
196                 component = Component.createComponent(child, parent);
197             } else if (child instanceof Element) {
198                 final Element el = (Element) child;
199                 if (ComponentResolver.isSnippet(el)) {
200                     component = create(el, parent);
201                 } else {
202                     component = Component.createComponent(el, parent);
203                     recurciveCreateComponent(el, component);
204                 }
205             }
206             if (component != null) {
207                 parent.addChild(component);
208             }
209         });
210     }
211 
212     protected void render(final SnippetComponent<?> component) {
213         traverseTee(component, c -> {
214             if (c instanceof SnippetComponent) {
215                 ((SnippetComponent<?>) c).render(this);
216             }
217         });
218         component.render(this);
219     }
220 
221     private void traverseTee(final Component<?> component, final Consumer<Component<?>> consumer) {
222         final Consumer<Component<?>> traverse = c -> traverseTee(c, consumer);
223         component.getChildren().forEach(consumer.andThen(traverse));
224     }
225 
226     protected String renderComponent(final SnippetComponent<?> component) {
227         final StringWriter writer = new StringWriter();
228         mergeTemplate(component, writer);
229         return writer.toString();
230     }
231 
232     protected void mergeTemplate(final SnippetComponent<?> component, final Writer writer) {
233         boolean found = false;
234         for (final String path : this.snippetPaths) {
235             final String filePath = path + '/' + component.getName() + ".vm";
236             if (Velocity.resourceExists(filePath)) {
237                 found = true;
238                 final Context context = createVelocityContext();
239                 context.put("snippet", component);
240                 context.put("snippetPath", filePath);
241                 context.put("config", this.config);
242                 context.put("velocity", Velocity.class);
243                 context.put("site", this.config.getSiteModel());
244                 // Use config option <absoluteResourceURL>http://mysite.com/</absoluteResourceURL>
245                 context.put("resourcePath", this.config.getResourcePath());
246 
247                 Velocity.mergeTemplate("META-INF/skin/snippets/_snippet.vm",
248                     RuntimeSingleton.getString(RuntimeConstants.INPUT_ENCODING, RuntimeConstants.ENCODING_DEFAULT),
249                     context,
250                     writer);
251                 break;
252             } else {
253                 if (LOGGER.isDebugEnabled()) {
254                     LOGGER.debug("Template for component '{}' not found in path:{} ", component, filePath);
255                 }
256             }
257         }
258         if (!found) {
259             LOGGER.warn("The snippet '{}' template doesn't exist", component.getName());
260         }
261     }
262 
263     /**
264      * Creates a Velocity Context with all generic tools configured wit the site rendering context.
265      *
266      * @return a Velocity tools managed context
267      */
268     protected Context createVelocityContext() {
269         return toolManager.createContext();
270     }
271 
272     /**
273      * @return
274      */
275     protected ToolManager createToolManaged() {
276 
277         final EasyFactoryConfiguration config = new EasyFactoryConfiguration(false);
278         config.property("safeMode", Boolean.FALSE);
279         config.toolbox(Scope.REQUEST)
280                 .tool(ContextTool.class)
281                 .tool(LinkTool.class)
282                 .tool(LoopTool.class)
283                 .tool(RenderTool.class);
284         config.toolbox(Scope.APPLICATION)
285                 .tool(ClassTool.class)
286                 .tool(ComparisonDateTool.class)
287                 .tool(DisplayTool.class)
288                 .tool(EscapeTool.class)
289                 .tool(FieldTool.class)
290                 .tool(MathTool.class)
291                 .tool(NumberTool.class)
292                 .tool(ResourceTool.class)
293                 .tool(XmlTool.class)
294                 .tool(URITool.class)
295                 .tool(HtmlTool.class);
296 
297         final ToolManager manager = new ToolManager(false, false);
298         manager.configure(config);
299         return manager;
300     }
301 }