    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}