001// Copyright 2007-2013 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 015package org.apache.tapestry5.internal.services; 016 017import org.apache.tapestry5.dom.Document; 018import org.apache.tapestry5.dom.Element; 019import org.apache.tapestry5.ioc.internal.util.CollectionFactory; 020import org.apache.tapestry5.json.JSONArray; 021import org.apache.tapestry5.services.javascript.InitializationPriority; 022import org.apache.tapestry5.services.javascript.ModuleConfigurationCallback; 023import org.apache.tapestry5.services.javascript.ModuleManager; 024import org.apache.tapestry5.services.javascript.StylesheetLink; 025 026import java.util.List; 027 028public class DocumentLinkerImpl implements DocumentLinker 029{ 030 private final List<String> coreLibraryURLs = CollectionFactory.newList(); 031 032 private final List<String> libraryURLs = CollectionFactory.newList(); 033 034 private final ModuleInitsManager initsManager = new ModuleInitsManager(); 035 036 private final List<ModuleConfigurationCallback> moduleConfigurationCallbacks = CollectionFactory.newList(); 037 038 private final List<StylesheetLink> includedStylesheets = CollectionFactory.newList(); 039 040 private final ModuleManager moduleManager; 041 042 private final boolean omitGeneratorMetaTag; 043 044 private final String tapestryBanner; 045 046 // Initially false; set to true when a scriptURL or any kind of initialization is added. 047 private boolean hasScriptsOrInitializations; 048 049 /** 050 * @param moduleManager 051 * used to identify the root folder for dynamically loaded modules 052 * @param omitGeneratorMetaTag 053 * via symbol configuration 054 * @param tapestryVersion 055 * version of Tapestry framework (for meta tag) 056 */ 057 public DocumentLinkerImpl(ModuleManager moduleManager, boolean omitGeneratorMetaTag, String tapestryVersion) 058 { 059 this.moduleManager = moduleManager; 060 this.omitGeneratorMetaTag = omitGeneratorMetaTag; 061 062 tapestryBanner = String.format("Apache Tapestry Framework (version %s)", tapestryVersion); 063 } 064 065 public void addStylesheetLink(StylesheetLink sheet) 066 { 067 includedStylesheets.add(sheet); 068 } 069 070 071 public void addCoreLibrary(String libraryURL) 072 { 073 coreLibraryURLs.add(libraryURL); 074 075 hasScriptsOrInitializations = true; 076 } 077 078 public void addLibrary(String libraryURL) 079 { 080 libraryURLs.add(libraryURL); 081 082 hasScriptsOrInitializations = true; 083 } 084 085 public void addScript(InitializationPriority priority, String script) 086 { 087 addInitialization(priority, "t5/core/pageinit", "evalJavaScript", new JSONArray().put(script)); 088 } 089 090 public void addInitialization(InitializationPriority priority, String moduleName, String functionName, JSONArray arguments) 091 { 092 initsManager.addInitialization(priority, moduleName, functionName, arguments); 093 094 hasScriptsOrInitializations = true; 095 } 096 097 /** 098 * Updates the supplied Document, possibly adding <head> or <body> elements. 099 * 100 * @param document 101 * to be updated 102 */ 103 public void updateDocument(Document document) 104 { 105 Element root = document.getRootElement(); 106 107 // If the document failed to render at all, that's a different problem and is reported elsewhere. 108 109 if (root == null) 110 { 111 return; 112 } 113 114 addStylesheetsToHead(root, includedStylesheets); 115 116 // only add the generator meta only to html documents 117 118 boolean isHtmlRoot = root.getName().equals("html"); 119 120 if (!omitGeneratorMetaTag && isHtmlRoot) 121 { 122 Element head = findOrCreateElement(root, "head", true); 123 124 Element existingMeta = head.find("meta"); 125 126 addElementBefore(head, existingMeta, "meta", "name", "generator", "content", tapestryBanner); 127 } 128 129 addScriptElements(root); 130 } 131 132 private static Element addElementBefore(Element container, Element insertionPoint, String name, String... namesAndValues) 133 { 134 if (insertionPoint == null) 135 { 136 return container.element(name, namesAndValues); 137 } 138 139 return insertionPoint.elementBefore(name, namesAndValues); 140 } 141 142 143 private void addScriptElements(Element root) 144 { 145 String rootElementName = root.getName(); 146 147 Element body = rootElementName.equals("html") ? findOrCreateElement(root, "body", false) : null; 148 149 // Write the data-page-initialized attribute in for all pages; it will be "true" when the page has no 150 // initializations (which is somewhat rare in Tapestry). When the page has initializations, it will be set to 151 // "true" once those initializations all run. 152 if (body != null) 153 { 154 body.attribute("data-page-initialized", Boolean.toString(!hasScriptsOrInitializations)); 155 } 156 157 if (!hasScriptsOrInitializations) 158 { 159 return; 160 } 161 162 // This only applies when the document is an HTML document. This may need to change in the 163 // future, perhaps configurable, to allow for html and xhtml and perhaps others. Does SVG 164 // use stylesheets? 165 166 if (!rootElementName.equals("html")) 167 { 168 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)); 169 } 170 171 // TAPESTRY-2364 172 173 addScriptsToEndOfBody(body); 174 } 175 176 /** 177 * Finds an element by name, or creates it. Returns the element (if found), or creates a new element 178 * with the given name, and returns it. The new element will be positioned at the top or bottom of the root element. 179 * 180 * @param root 181 * element to search 182 * @param childElement 183 * element name of child 184 * @param atTop 185 * if not found, create new element at top of root, or at bottom 186 * @return the located element, or null 187 */ 188 private Element findOrCreateElement(Element root, String childElement, boolean atTop) 189 { 190 Element container = root.find(childElement); 191 192 // Create the element is it is missing. 193 194 if (container == null) 195 { 196 container = atTop ? root.elementAt(0, childElement) : root.element(childElement); 197 } 198 199 return container; 200 } 201 202 203 /** 204 * Adds {@code <script>} elements for the RequireJS library, then any statically includes JavaScript libraries 205 * (including JavaScript stack virtual assets), then the initialization script block. 206 * 207 * @param body 208 * element to add the dynamic scripting to 209 */ 210 protected void addScriptsToEndOfBody(Element body) 211 { 212 moduleManager.writeConfiguration(body, moduleConfigurationCallbacks); 213 214 // Write the core libraries, which includes RequireJS: 215 216 for (String url : coreLibraryURLs) 217 { 218 body.element("script", 219 "type", "text/javascript", 220 "src", url); 221 } 222 223 // Write the initialization at this point. 224 225 moduleManager.writeInitialization(body, libraryURLs, initsManager.getSortedInits()); 226 } 227 228 private static Element createTemporaryContainer(Element headElement, String existingElementName, String newElementName) 229 { 230 Element existingScript = headElement.find(existingElementName); 231 232 // Create temporary container for the new <script> elements 233 234 return addElementBefore(headElement, existingScript, newElementName); 235 } 236 237 /** 238 * Locates the head element under the root ("html") element, creating it if necessary, and adds the stylesheets to 239 * it. 240 * 241 * @param root 242 * element of document 243 * @param stylesheets 244 * to add to the document 245 */ 246 protected void addStylesheetsToHead(Element root, List<StylesheetLink> stylesheets) 247 { 248 int count = stylesheets.size(); 249 250 if (count == 0) 251 { 252 return; 253 } 254 255 // This only applies when the document is an HTML document. This may need to change in the 256 // future, perhaps configurable, to allow for html and xhtml and perhaps others. Does SVG 257 // use stylesheets? 258 259 String rootElementName = root.getName(); 260 261 // Not an html document, don't add anything. 262 if (!rootElementName.equals("html")) 263 { 264 return; 265 } 266 267 Element head = findOrCreateElement(root, "head", true); 268 269 // Create a temporary container element. 270 Element container = createTemporaryContainer(head, "style", "stylesheet-container"); 271 272 for (int i = 0; i < count; i++) 273 { 274 stylesheets.get(i).add(container); 275 } 276 277 container.pop(); 278 } 279 280 public void addModuleConfigurationCallback(ModuleConfigurationCallback callback) 281 { 282 assert callback != null; 283 moduleConfigurationCallbacks.add(callback); 284 } 285 286}