001// Copyright 2007, 2008, 2009, 2010 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007// http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014
015package org.apache.tapestry5.internal.services;
016
017import org.apache.tapestry5.dom.Document;
018import org.apache.tapestry5.dom.Element;
019import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
020import org.apache.tapestry5.json.JSONObject;
021import org.apache.tapestry5.services.javascript.InitializationPriority;
022import org.apache.tapestry5.services.javascript.StylesheetLink;
023
024import java.util.List;
025import java.util.Map;
026
027public class DocumentLinkerImpl implements DocumentLinker
028{
029    private final List<String> scripts = CollectionFactory.newList();
030
031    private final Map<InitializationPriority, StringBuilder> priorityToScript = CollectionFactory.newMap();
032
033    private final Map<InitializationPriority, JSONObject> priorityToInit = CollectionFactory.newMap();
034
035    private final List<StylesheetLink> includedStylesheets = CollectionFactory.newList();
036
037    private final boolean compactJSON;
038
039    private final boolean omitGeneratorMetaTag;
040
041    private final String tapestryBanner;
042
043    private boolean hasDynamicScript;
044
045    /**
046     * @param omitGeneratorMetaTag via symbol configuration
047     * @param tapestryVersion      version of Tapestry framework (for meta tag)
048     * @param compactJSON          should JSON content be compact or pretty printed?
049     */
050    public DocumentLinkerImpl(boolean omitGeneratorMetaTag, String tapestryVersion, boolean compactJSON)
051    {
052        this.omitGeneratorMetaTag = omitGeneratorMetaTag;
053
054        tapestryBanner = String.format("Apache Tapestry Framework (version %s)", tapestryVersion);
055
056        this.compactJSON = compactJSON;
057    }
058
059    public void addStylesheetLink(StylesheetLink sheet)
060    {
061        includedStylesheets.add(sheet);
062    }
063
064    public void addScriptLink(String scriptURL)
065    {
066        scripts.add(scriptURL);
067    }
068
069    public void addScript(InitializationPriority priority, String script)
070    {
071
072        StringBuilder builder = priorityToScript.get(priority);
073
074        if (builder == null)
075        {
076            builder = new StringBuilder();
077            priorityToScript.put(priority, builder);
078        }
079
080        builder.append(script);
081
082        builder.append("\n");
083
084        hasDynamicScript = true;
085    }
086
087    public void setInitialization(InitializationPriority priority, JSONObject initialization)
088    {
089        priorityToInit.put(priority, initialization);
090
091        hasDynamicScript = true;
092    }
093
094    /**
095     * Updates the supplied Document, possibly adding &lt;head&gt; or &lt;body&gt; elements.
096     *
097     * @param document to be updated
098     */
099    public void updateDocument(Document document)
100    {
101        Element root = document.getRootElement();
102
103        // If the document failed to render at all, that's a different problem and is reported elsewhere.
104
105        if (root == null)
106            return;
107
108        addStylesheetsToHead(root, includedStylesheets);
109
110        // only add the generator meta only to html documents
111
112        boolean isHtmlRoot = root.getName().equals("html");
113
114        if (!omitGeneratorMetaTag && isHtmlRoot)
115        {
116            Element head = findOrCreateElement(root, "head", true);
117
118            Element existingMeta = head.find("meta");
119
120            addElementBefore(head, existingMeta, "meta", "name", "generator", "content", tapestryBanner);
121        }
122
123        addScriptElements(root);
124    }
125
126    private static Element addElementBefore(Element container, Element insertionPoint, String name, String... namesAndValues)
127    {
128        if (insertionPoint == null)
129        {
130            return container.element(name, namesAndValues);
131        }
132
133        return insertionPoint.elementBefore(name, namesAndValues);
134    }
135
136
137    private void addScriptElements(Element root)
138    {
139        if (scripts.isEmpty() && !hasDynamicScript)
140            return;
141
142        // This only applies when the document is an HTML document. This may need to change in the
143        // future, perhaps configurable, to allow for html and xhtml and perhaps others. Does SVG
144        // use stylesheets?
145
146        String rootElementName = root.getName();
147
148        if (!rootElementName.equals("html"))
149            throw new RuntimeException(ServicesMessages.documentMissingHTMLRoot(rootElementName));
150
151        Element head = findOrCreateElement(root, "head", true);
152
153        // TAPESTRY-2364
154
155        addScriptLinksForIncludedScripts(head, scripts);
156
157        if (hasDynamicScript)
158            addDynamicScriptBlock(findOrCreateElement(root, "body", false));
159    }
160
161    /**
162     * Finds an element by name, or creates it. Returns the element (if found), or creates a new element
163     * with the given name, and returns it. The new element will be positioned at the top or bottom of the root element.
164     *
165     * @param root         element to search
166     * @param childElement element name of child
167     * @param atTop        if not found, create new element at top of root, or at bottom
168     * @return the located element, or null
169     */
170    private Element findOrCreateElement(Element root, String childElement, boolean atTop)
171    {
172        Element container = root.find(childElement);
173
174        // Create the element is it is missing.
175
176        if (container == null)
177            container = atTop ? root.elementAt(0, childElement) : root.element(childElement);
178
179        return container;
180    }
181
182    /**
183     * Adds the dynamic script block, which is, ultimately, a call to the client-side Tapestry.onDOMLoaded() function.
184     *
185     * @param body element to add the dynamic scripting to
186     */
187    protected void addDynamicScriptBlock(Element body)
188    {
189        StringBuilder block = new StringBuilder();
190
191        boolean wrapped = false;
192
193        for (InitializationPriority p : InitializationPriority.values())
194        {
195            if (p != InitializationPriority.IMMEDIATE && !wrapped
196                    && (priorityToScript.containsKey(p) || priorityToInit.containsKey(p)))
197            {
198
199                block.append("Tapestry.onDOMLoaded(function() {\n");
200
201                wrapped = true;
202            }
203
204            add(block, p);
205        }
206
207        if (wrapped)
208            block.append("});\n");
209
210        Element e = body.element("script", "type", "text/javascript");
211
212        e.raw(block.toString());
213
214    }
215
216    private void add(StringBuilder block, InitializationPriority priority)
217    {
218        add(block, priorityToScript.get(priority));
219        add(block, priorityToInit.get(priority));
220    }
221
222    private void add(StringBuilder block, JSONObject init)
223    {
224        if (init == null)
225            return;
226
227        block.append("Tapestry.init(");
228        block.append(init.toString(compactJSON));
229        block.append(");\n");
230    }
231
232    private void add(StringBuilder block, StringBuilder content)
233    {
234        if (content == null)
235            return;
236
237        block.append(content);
238    }
239
240    /**
241     * Adds a script link for each included script to the top of the the {@code <head>} element.
242     * The new elements are inserted just before the first {@code <script>} tag, or appended at
243     * the end.
244     *
245     * @param headElement element to add the script links to
246     * @param scripts     scripts URLs to add as {@code <script>} elements
247     */
248    protected void addScriptLinksForIncludedScripts(final Element headElement, List<String> scripts)
249    {
250        // TAP5-1486
251
252        // Find the first existing <script> tag if it exists.
253
254        Element container = createTemporaryContainer(headElement, "script", "script-container");
255
256        for (String script : scripts)
257        {
258            container.element("script", "type", "text/javascript", "src", script);
259        }
260
261        container.pop();
262    }
263
264    private static Element createTemporaryContainer(Element headElement, String existingElementName, String newElementName)
265    {
266        Element existingScript = headElement.find(existingElementName);
267
268        // Create temporary container for the new <script> elements
269
270        return addElementBefore(headElement, existingScript, newElementName);
271    }
272
273    /**
274     * Locates the head element under the root ("html") element, creating it if necessary, and adds the stylesheets to
275     * it.
276     *
277     * @param root        element of document
278     * @param stylesheets to add to the document
279     */
280    protected void addStylesheetsToHead(Element root, List<StylesheetLink> stylesheets)
281    {
282        int count = stylesheets.size();
283
284        if (count == 0)
285        {
286            return;
287        }
288
289        // This only applies when the document is an HTML document. This may need to change in the
290        // future, perhaps configurable, to allow for html and xhtml and perhaps others. Does SVG
291        // use stylesheets?
292
293        String rootElementName = root.getName();
294
295        // Not an html document, don't add anything.
296        if (!rootElementName.equals("html"))
297        {
298            return;
299        }
300
301        Element head = findOrCreateElement(root, "head", true);
302
303        // Create a temporary container element.
304        Element container = createTemporaryContainer(head, "style", "stylesheet-container");
305
306        for (int i = 0; i < count; i++)
307        {
308            stylesheets.get(i).add(container);
309        }
310
311        container.pop();
312    }
313}