001// Copyright 2007-2013 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.JSONArray;
021import org.apache.tapestry5.services.javascript.InitializationPriority;
022import org.apache.tapestry5.services.javascript.ModuleConfigurationCallback;
023import org.apache.tapestry5.services.javascript.ModuleManager;
024import org.apache.tapestry5.services.javascript.StylesheetLink;
025
026import java.util.List;
027
028public class DocumentLinkerImpl implements DocumentLinker
029{
030    private final List<String> coreLibraryURLs = CollectionFactory.newList();
031
032    private final List<String> libraryURLs = CollectionFactory.newList();
033
034    private final ModuleInitsManager initsManager = new ModuleInitsManager();
035    
036    private final List<ModuleConfigurationCallback> moduleConfigurationCallbacks = CollectionFactory.newList();
037
038    private final List<StylesheetLink> includedStylesheets = CollectionFactory.newList();
039
040    private final ModuleManager moduleManager;
041
042    private final boolean omitGeneratorMetaTag;
043
044    private final String tapestryBanner;
045
046    // Initially false; set to true when a scriptURL or any kind of initialization is added.
047    private boolean hasScriptsOrInitializations;
048
049    /**
050     * @param moduleManager
051     *         used to identify the root folder for dynamically loaded modules
052     * @param omitGeneratorMetaTag
053     *         via symbol configuration
054     * @param tapestryVersion
055     *         version of Tapestry framework (for meta tag)
056     */
057    public DocumentLinkerImpl(ModuleManager moduleManager, boolean omitGeneratorMetaTag, String tapestryVersion)
058    {
059        this.moduleManager = moduleManager;
060        this.omitGeneratorMetaTag = omitGeneratorMetaTag;
061
062        tapestryBanner = String.format("Apache Tapestry Framework (version %s)", tapestryVersion);
063    }
064
065    public void addStylesheetLink(StylesheetLink sheet)
066    {
067        includedStylesheets.add(sheet);
068    }
069
070
071    public void addCoreLibrary(String libraryURL)
072    {
073        coreLibraryURLs.add(libraryURL);
074
075        hasScriptsOrInitializations = true;
076    }
077
078    public void addLibrary(String libraryURL)
079    {
080        libraryURLs.add(libraryURL);
081
082        hasScriptsOrInitializations = true;
083    }
084
085    public void addScript(InitializationPriority priority, String script)
086    {
087        addInitialization(priority, "t5/core/pageinit", "evalJavaScript", new JSONArray().put(script));
088    }
089
090    public void addInitialization(InitializationPriority priority, String moduleName, String functionName, JSONArray arguments)
091    {
092        initsManager.addInitialization(priority, moduleName, functionName, arguments);
093
094        hasScriptsOrInitializations = true;
095    }
096
097    /**
098     * Updates the supplied Document, possibly adding &lt;head&gt; or &lt;body&gt; elements.
099     *
100     * @param document
101     *         to be updated
102     */
103    public void updateDocument(Document document)
104    {
105        Element root = document.getRootElement();
106
107        // If the document failed to render at all, that's a different problem and is reported elsewhere.
108
109        if (root == null)
110        {
111            return;
112        }
113
114        addStylesheetsToHead(root, includedStylesheets);
115
116        // only add the generator meta only to html documents
117
118        boolean isHtmlRoot = root.getName().equals("html");
119
120        if (!omitGeneratorMetaTag && isHtmlRoot)
121        {
122            Element head = findOrCreateElement(root, "head", true);
123
124            Element existingMeta = head.find("meta");
125
126            addElementBefore(head, existingMeta, "meta", "name", "generator", "content", tapestryBanner);
127        }
128
129        addScriptElements(root);
130    }
131
132    private static Element addElementBefore(Element container, Element insertionPoint, String name, String... namesAndValues)
133    {
134        if (insertionPoint == null)
135        {
136            return container.element(name, namesAndValues);
137        }
138
139        return insertionPoint.elementBefore(name, namesAndValues);
140    }
141
142
143    private void addScriptElements(Element root)
144    {
145        String rootElementName = root.getName();
146
147        Element body = rootElementName.equals("html") ? findOrCreateElement(root, "body", false) : null;
148
149        // Write the data-page-initialized attribute in for all pages; it will be "true" when the page has no
150        // initializations (which is somewhat rare in Tapestry). When the page has initializations, it will be set to
151        // "true" once those initializations all run.
152        if (body != null)
153        {
154            body.attribute("data-page-initialized", Boolean.toString(!hasScriptsOrInitializations));
155        }
156
157        if (!hasScriptsOrInitializations)
158        {
159            return;
160        }
161
162        // This only applies when the document is an HTML document. This may need to change in the
163        // future, perhaps configurable, to allow for html and xhtml and perhaps others. Does SVG
164        // use stylesheets?
165
166        if (!rootElementName.equals("html"))
167        {
168            throw new RuntimeException(String.format("The root element of the rendered document was <%s>, not <html>. A root element of <html> is needed when linking JavaScript and stylesheet resources.", rootElementName));
169        }
170
171        // TAPESTRY-2364
172
173        addScriptsToEndOfBody(body);
174    }
175
176    /**
177     * Finds an element by name, or creates it. Returns the element (if found), or creates a new element
178     * with the given name, and returns it. The new element will be positioned at the top or bottom of the root element.
179     *
180     * @param root
181     *         element to search
182     * @param childElement
183     *         element name of child
184     * @param atTop
185     *         if not found, create new element at top of root, or at bottom
186     * @return the located element, or null
187     */
188    private Element findOrCreateElement(Element root, String childElement, boolean atTop)
189    {
190        Element container = root.find(childElement);
191
192        // Create the element is it is missing.
193
194        if (container == null)
195        {
196            container = atTop ? root.elementAt(0, childElement) : root.element(childElement);
197        }
198
199        return container;
200    }
201
202
203    /**
204     * Adds {@code <script>} elements for the RequireJS library, then any statically includes JavaScript libraries
205     * (including JavaScript stack virtual assets), then the initialization script block.
206     *
207     * @param body
208     *         element to add the dynamic scripting to
209     */
210    protected void addScriptsToEndOfBody(Element body)
211    {
212        moduleManager.writeConfiguration(body, moduleConfigurationCallbacks);
213
214        // Write the core libraries, which includes RequireJS:
215
216        for (String url : coreLibraryURLs)
217        {
218            body.element("script",
219                    "type", "text/javascript",
220                    "src", url);
221        }
222
223        // Write the initialization at this point.
224
225        moduleManager.writeInitialization(body, libraryURLs, initsManager.getSortedInits());
226    }
227
228    private static Element createTemporaryContainer(Element headElement, String existingElementName, String newElementName)
229    {
230        Element existingScript = headElement.find(existingElementName);
231
232        // Create temporary container for the new <script> elements
233
234        return addElementBefore(headElement, existingScript, newElementName);
235    }
236
237    /**
238     * Locates the head element under the root ("html") element, creating it if necessary, and adds the stylesheets to
239     * it.
240     *
241     * @param root
242     *         element of document
243     * @param stylesheets
244     *         to add to the document
245     */
246    protected void addStylesheetsToHead(Element root, List<StylesheetLink> stylesheets)
247    {
248        int count = stylesheets.size();
249
250        if (count == 0)
251        {
252            return;
253        }
254
255        // This only applies when the document is an HTML document. This may need to change in the
256        // future, perhaps configurable, to allow for html and xhtml and perhaps others. Does SVG
257        // use stylesheets?
258
259        String rootElementName = root.getName();
260
261        // Not an html document, don't add anything.
262        if (!rootElementName.equals("html"))
263        {
264            return;
265        }
266
267        Element head = findOrCreateElement(root, "head", true);
268
269        // Create a temporary container element.
270        Element container = createTemporaryContainer(head, "style", "stylesheet-container");
271
272        for (int i = 0; i < count; i++)
273        {
274            stylesheets.get(i).add(container);
275        }
276
277        container.pop();
278    }
279
280    public void addModuleConfigurationCallback(ModuleConfigurationCallback callback)
281    {
282        assert callback != null;
283        moduleConfigurationCallbacks.add(callback);
284    }
285    
286}