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 }