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    }