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 <head> or <body> 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 }