    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
    015    package org.apache.tapestry5.internal.services;
    016
    017    import org.apache.tapestry5.dom.Document;
    018    import org.apache.tapestry5.dom.Element;
    019    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
    020    import org.apache.tapestry5.json.JSONObject;
    021    import org.apache.tapestry5.services.javascript.InitializationPriority;
    022    import org.apache.tapestry5.services.javascript.StylesheetLink;
    023
    024    import java.util.List;
    025    import java.util.Map;
    026
    027    public 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    }