001// Licensed under the Apache License, Version 2.0 (the "License");
002// you may not use this file except in compliance with the License.
003// You may obtain a copy of the License at
004//
005// http://www.apache.org/licenses/LICENSE-2.0
006//
007// Unless required by applicable law or agreed to in writing, software
008// distributed under the License is distributed on an "AS IS" BASIS,
009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
010// See the License for the specific language governing permissions and
011// limitations under the License.
012
013package org.apache.tapestry5.internal.services;
014
015import org.apache.tapestry5.commons.util.CollectionFactory;
016import org.apache.tapestry5.dom.Document;
017import org.apache.tapestry5.dom.Element;
018import org.apache.tapestry5.json.JSONArray;
019import org.apache.tapestry5.services.javascript.InitializationPriority;
020import org.apache.tapestry5.services.javascript.ModuleConfigurationCallback;
021import org.apache.tapestry5.services.javascript.ModuleManager;
022import org.apache.tapestry5.services.javascript.StylesheetLink;
023
024import java.util.List;
025import java.util.Set;
026
027public class DocumentLinkerImpl implements DocumentLinker
028{
029
030    private final static Set<String> HTML_MIME_TYPES = CollectionFactory.newSet("text/html", "application/xml+xhtml");
031
032    private final List<String> coreLibraryURLs = CollectionFactory.newList();
033
034    private final List<String> libraryURLs = CollectionFactory.newList();
035
036    private final ModuleInitsManager initsManager = new ModuleInitsManager();
037
038    private final List<ModuleConfigurationCallback> moduleConfigurationCallbacks = CollectionFactory.newList();
039
040    private final List<StylesheetLink> includedStylesheets = CollectionFactory.newList();
041
042    private final ModuleManager moduleManager;
043
044    private final boolean omitGeneratorMetaTag, enablePageloadingMask;
045
046    private final String tapestryBanner;
047
048    // Initially false; set to true when a scriptURL or any kind of initialization is added.
049    private boolean hasScriptsOrInitializations;
050
051    /**
052     * @param moduleManager
053     *         used to identify the root folder for dynamically loaded modules
054     * @param omitGeneratorMetaTag
055     *         via symbol configuration
056     * @param enablePageloadingMask
057     * @param tapestryVersion
058     */
059    public DocumentLinkerImpl(ModuleManager moduleManager, boolean omitGeneratorMetaTag, boolean enablePageloadingMask, String tapestryVersion)
060    {
061        this.moduleManager = moduleManager;
062        this.omitGeneratorMetaTag = omitGeneratorMetaTag;
063        this.enablePageloadingMask = enablePageloadingMask;
064
065        tapestryBanner = "Apache Tapestry Framework (version " + tapestryVersion + ')';
066    }
067
068    public void addStylesheetLink(StylesheetLink sheet)
069    {
070        includedStylesheets.add(sheet);
071    }
072
073
074    public void addCoreLibrary(String libraryURL)
075    {
076        coreLibraryURLs.add(libraryURL);
077
078        hasScriptsOrInitializations = true;
079    }
080
081    public void addLibrary(String libraryURL)
082    {
083        libraryURLs.add(libraryURL);
084
085        hasScriptsOrInitializations = true;
086    }
087
088    public void addScript(InitializationPriority priority, String script)
089    {
090        addInitialization(priority, "t5/core/pageinit", "evalJavaScript", new JSONArray().put(script));
091    }
092
093    public void addInitialization(InitializationPriority priority, String moduleName, String functionName, JSONArray arguments)
094    {
095        initsManager.addInitialization(priority, moduleName, functionName, arguments);
096
097        hasScriptsOrInitializations = true;
098    }
099
100    /**
101     * Updates the supplied Document, possibly adding &lt;head&gt; or &lt;body&gt; elements.
102     *
103     * @param document
104     *         to be updated
105     */
106    public void updateDocument(Document document)
107    {
108        Element root = document.getRootElement();
109
110        // If the document failed to render at all, that's a different problem and is reported elsewhere.
111
112        if (root == null)
113        {
114            return;
115        }
116
117        // TAP5-2200: Generating XML from pages and templates is not possible anymore
118        // only add JavaScript and CSS if we're actually generating 
119        final String mimeType = document.getMimeType();
120        if (mimeType != null && !HTML_MIME_TYPES.contains(mimeType))
121        {
122            return;
123        }
124
125        addStylesheetsToHead(root, includedStylesheets);
126
127        // only add the generator meta only to html documents
128
129        boolean isHtmlRoot = root.getName().equals("html");
130
131        if (!omitGeneratorMetaTag && isHtmlRoot)
132        {
133            Element head = findOrCreateElement(root, "head", true);
134
135            Element existingMeta = head.find("meta");
136
137            addElementBefore(head, existingMeta, "meta", "name", "generator", "content", tapestryBanner);
138        }
139
140        addScriptElements(root);
141    }
142
143    private static Element addElementBefore(Element container, Element insertionPoint, String name, String... namesAndValues)
144    {
145        if (insertionPoint == null)
146        {
147            return container.element(name, namesAndValues);
148        }
149
150        return insertionPoint.elementBefore(name, namesAndValues);
151    }
152
153
154    private void addScriptElements(Element root)
155    {
156        String rootElementName = root.getName();
157
158        Element body = rootElementName.equals("html") ? findOrCreateElement(root, "body", false) : null;
159
160        // Write the data-page-initialized attribute in for all pages; it will be "true" when the page has no
161        // initializations (which is somewhat rare in Tapestry). When the page has initializations, it will be set to
162        // "true" once those initializations all run.
163        if (body != null)
164        {
165            body.attribute("data-page-initialized", Boolean.toString(!hasScriptsOrInitializations));
166        }
167
168        if (!hasScriptsOrInitializations)
169        {
170            return;
171        }
172
173        // This only applies when the document is an HTML document. This may need to change in the
174        // future, perhaps configurable, to allow for html and xhtml and perhaps others. Does SVG
175        // use stylesheets?
176
177        if (!rootElementName.equals("html"))
178        {
179            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));
180        }
181
182        // TAPESTRY-2364
183
184        addContentToBody(body);
185    }
186
187    /**
188     * Finds an element by name, or creates it. Returns the element (if found), or creates a new element
189     * with the given name, and returns it. The new element will be positioned at the top or bottom of the root element.
190     *
191     * @param root
192     *         element to search
193     * @param childElement
194     *         element name of child
195     * @param atTop
196     *         if not found, create new element at top of root, or at bottom
197     * @return the located element, or null
198     */
199    private Element findOrCreateElement(Element root, String childElement, boolean atTop)
200    {
201        Element container = root.find(childElement);
202
203        // Create the element is it is missing.
204
205        if (container == null)
206        {
207            container = atTop ? root.elementAt(0, childElement) : root.element(childElement);
208        }
209
210        return container;
211    }
212
213
214    /**
215     * Adds {@code <script>} elements for the RequireJS library, then any statically includes JavaScript libraries
216     * (including JavaScript stack virtual assets), then the initialization script block.
217     *
218     * @param body
219     *         element to add the dynamic scripting to
220     */
221    protected void addContentToBody(Element body)
222    {
223        if (enablePageloadingMask)
224        {
225            // This adds a mask element to the page, based on the Bootstrap modal dialog backdrop. The mark
226            // is present immediately, but fades in visually after a short delay, and is removed
227            // after page initialization is complete. For a client that doesn't have JavaScript enabled,
228            // this will do nothing (though I suspect the page will not behave to expectations!).
229            Element script = body.element("script", "type", "text/javascript");
230            script.raw("document.write(\"<div class=\\\"pageloading-mask\\\"><div></div></div>\");");
231
232            script.moveToTop(body);
233        }
234
235        moduleManager.writeConfiguration(body, moduleConfigurationCallbacks);
236
237        // Write the core libraries, which includes RequireJS:
238
239        for (String url : coreLibraryURLs)
240        {
241            body.element("script",
242                    "type", "text/javascript",
243                    "src", url);
244        }
245
246        // Write the initialization at this point.
247
248        moduleManager.writeInitialization(body, libraryURLs, initsManager.getSortedInits());
249    }
250
251    private static Element createTemporaryContainer(Element headElement, String existingElementName, String newElementName)
252    {
253        Element existingScript = headElement.find(existingElementName);
254
255        // Create temporary container for the new <script> elements
256
257        return addElementBefore(headElement, existingScript, newElementName);
258    }
259
260    /**
261     * Locates the head element under the root ("html") element, creating it if necessary, and adds the stylesheets to
262     * it.
263     *
264     * @param root
265     *         element of document
266     * @param stylesheets
267     *         to add to the document
268     */
269    protected void addStylesheetsToHead(Element root, List<StylesheetLink> stylesheets)
270    {
271        int count = stylesheets.size();
272
273        if (count == 0)
274        {
275            return;
276        }
277
278        // This only applies when the document is an HTML document. This may need to change in the
279        // future, perhaps configurable, to allow for html and xhtml and perhaps others. Does SVG
280        // use stylesheets?
281
282        String rootElementName = root.getName();
283
284        // Not an html document, don't add anything.
285        if (!rootElementName.equals("html"))
286        {
287            return;
288        }
289
290        Element head = findOrCreateElement(root, "head", true);
291
292        // Create a temporary container element.
293        Element container = createTemporaryContainer(head, "style", "stylesheet-container");
294
295        for (int i = 0; i < count; i++)
296        {
297            stylesheets.get(i).add(container);
298        }
299
300        container.pop();
301    }
302
303    public void addModuleConfigurationCallback(ModuleConfigurationCallback callback)
304    {
305        assert callback != null;
306        moduleConfigurationCallbacks.add(callback);
307    }
308
309}