001// Licensed under the Apache License, Version 2.0 (the "License"); 002// you may not use this file except in compliance with the License. 003// You may obtain a copy of the License at 004// 005// http://www.apache.org/licenses/LICENSE-2.0 006// 007// Unless required by applicable law or agreed to in writing, software 008// distributed under the License is distributed on an "AS IS" BASIS, 009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 010// See the License for the specific language governing permissions and 011// limitations under the License. 012 013package org.apache.tapestry5.internal.services; 014 015import org.apache.tapestry5.dom.Document; 016import org.apache.tapestry5.dom.Element; 017import org.apache.tapestry5.ioc.internal.util.CollectionFactory; 018import org.apache.tapestry5.json.JSONArray; 019import org.apache.tapestry5.services.javascript.InitializationPriority; 020import org.apache.tapestry5.services.javascript.ModuleConfigurationCallback; 021import org.apache.tapestry5.services.javascript.ModuleManager; 022import org.apache.tapestry5.services.javascript.StylesheetLink; 023 024import java.util.List; 025import java.util.Set; 026 027public class DocumentLinkerImpl implements DocumentLinker 028{ 029 030 private final static Set<String> HTML_MIME_TYPES = CollectionFactory.newSet("text/html", "application/xml+xhtml"); 031 032 private final List<String> coreLibraryURLs = CollectionFactory.newList(); 033 034 private final List<String> libraryURLs = CollectionFactory.newList(); 035 036 private final ModuleInitsManager initsManager = new ModuleInitsManager(); 037 038 private final List<ModuleConfigurationCallback> moduleConfigurationCallbacks = CollectionFactory.newList(); 039 040 private final List<StylesheetLink> includedStylesheets = CollectionFactory.newList(); 041 042 private final ModuleManager moduleManager; 043 044 private final boolean omitGeneratorMetaTag, enablePageloadingMask; 045 046 private final String tapestryBanner; 047 048 // Initially false; set to true when a scriptURL or any kind of initialization is added. 049 private boolean hasScriptsOrInitializations; 050 051 /** 052 * @param moduleManager 053 * used to identify the root folder for dynamically loaded modules 054 * @param omitGeneratorMetaTag 055 * via symbol configuration 056 * @param enablePageloadingMask 057 * @param tapestryVersion 058 */ 059 public DocumentLinkerImpl(ModuleManager moduleManager, boolean omitGeneratorMetaTag, boolean enablePageloadingMask, String tapestryVersion) 060 { 061 this.moduleManager = moduleManager; 062 this.omitGeneratorMetaTag = omitGeneratorMetaTag; 063 this.enablePageloadingMask = enablePageloadingMask; 064 065 tapestryBanner = "Apache Tapestry Framework (version " + tapestryVersion + ')'; 066 } 067 068 public void addStylesheetLink(StylesheetLink sheet) 069 { 070 includedStylesheets.add(sheet); 071 } 072 073 074 public void addCoreLibrary(String libraryURL) 075 { 076 coreLibraryURLs.add(libraryURL); 077 078 hasScriptsOrInitializations = true; 079 } 080 081 public void addLibrary(String libraryURL) 082 { 083 libraryURLs.add(libraryURL); 084 085 hasScriptsOrInitializations = true; 086 } 087 088 public void addScript(InitializationPriority priority, String script) 089 { 090 addInitialization(priority, "t5/core/pageinit", "evalJavaScript", new JSONArray().put(script)); 091 } 092 093 public void addInitialization(InitializationPriority priority, String moduleName, String functionName, JSONArray arguments) 094 { 095 initsManager.addInitialization(priority, moduleName, functionName, arguments); 096 097 hasScriptsOrInitializations = true; 098 } 099 100 /** 101 * Updates the supplied Document, possibly adding <head> or <body> elements. 102 * 103 * @param document 104 * to be updated 105 */ 106 public void updateDocument(Document document) 107 { 108 Element root = document.getRootElement(); 109 110 // If the document failed to render at all, that's a different problem and is reported elsewhere. 111 112 if (root == null) 113 { 114 return; 115 } 116 117 // TAP5-2200: Generating XML from pages and templates is not possible anymore 118 // only add JavaScript and CSS if we're actually generating 119 final String mimeType = document.getMimeType(); 120 if (mimeType != null && !HTML_MIME_TYPES.contains(mimeType)) 121 { 122 return; 123 } 124 125 addStylesheetsToHead(root, includedStylesheets); 126 127 // only add the generator meta only to html documents 128 129 boolean isHtmlRoot = root.getName().equals("html"); 130 131 if (!omitGeneratorMetaTag && isHtmlRoot) 132 { 133 Element head = findOrCreateElement(root, "head", true); 134 135 Element existingMeta = head.find("meta"); 136 137 addElementBefore(head, existingMeta, "meta", "name", "generator", "content", tapestryBanner); 138 } 139 140 addScriptElements(root); 141 } 142 143 private static Element addElementBefore(Element container, Element insertionPoint, String name, String... namesAndValues) 144 { 145 if (insertionPoint == null) 146 { 147 return container.element(name, namesAndValues); 148 } 149 150 return insertionPoint.elementBefore(name, namesAndValues); 151 } 152 153 154 private void addScriptElements(Element root) 155 { 156 String rootElementName = root.getName(); 157 158 Element body = rootElementName.equals("html") ? findOrCreateElement(root, "body", false) : null; 159 160 // Write the data-page-initialized attribute in for all pages; it will be "true" when the page has no 161 // initializations (which is somewhat rare in Tapestry). When the page has initializations, it will be set to 162 // "true" once those initializations all run. 163 if (body != null) 164 { 165 body.attribute("data-page-initialized", Boolean.toString(!hasScriptsOrInitializations)); 166 } 167 168 if (!hasScriptsOrInitializations) 169 { 170 return; 171 } 172 173 // This only applies when the document is an HTML document. This may need to change in the 174 // future, perhaps configurable, to allow for html and xhtml and perhaps others. Does SVG 175 // use stylesheets? 176 177 if (!rootElementName.equals("html")) 178 { 179 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)); 180 } 181 182 // TAPESTRY-2364 183 184 addContentToBody(body); 185 } 186 187 /** 188 * Finds an element by name, or creates it. Returns the element (if found), or creates a new element 189 * with the given name, and returns it. The new element will be positioned at the top or bottom of the root element. 190 * 191 * @param root 192 * element to search 193 * @param childElement 194 * element name of child 195 * @param atTop 196 * if not found, create new element at top of root, or at bottom 197 * @return the located element, or null 198 */ 199 private Element findOrCreateElement(Element root, String childElement, boolean atTop) 200 { 201 Element container = root.find(childElement); 202 203 // Create the element is it is missing. 204 205 if (container == null) 206 { 207 container = atTop ? root.elementAt(0, childElement) : root.element(childElement); 208 } 209 210 return container; 211 } 212 213 214 /** 215 * Adds {@code <script>} elements for the RequireJS library, then any statically includes JavaScript libraries 216 * (including JavaScript stack virtual assets), then the initialization script block. 217 * 218 * @param body 219 * element to add the dynamic scripting to 220 */ 221 protected void addContentToBody(Element body) 222 { 223 if (enablePageloadingMask) 224 { 225 // This adds a mask element to the page, based on the Bootstrap modal dialog backdrop. The mark 226 // is present immediately, but fades in visually after a short delay, and is removed 227 // after page initialization is complete. For a client that doesn't have JavaScript enabled, 228 // this will do nothing (though I suspect the page will not behave to expectations!). 229 Element script = body.element("script", "type", "text/javascript"); 230 script.raw("document.write(\"<div class=\\\"pageloading-mask\\\"><div></div></div>\");"); 231 232 script.moveToTop(body); 233 } 234 235 moduleManager.writeConfiguration(body, moduleConfigurationCallbacks); 236 237 // Write the core libraries, which includes RequireJS: 238 239 for (String url : coreLibraryURLs) 240 { 241 body.element("script", 242 "type", "text/javascript", 243 "src", url); 244 } 245 246 // Write the initialization at this point. 247 248 moduleManager.writeInitialization(body, libraryURLs, initsManager.getSortedInits()); 249 } 250 251 private static Element createTemporaryContainer(Element headElement, String existingElementName, String newElementName) 252 { 253 Element existingScript = headElement.find(existingElementName); 254 255 // Create temporary container for the new <script> elements 256 257 return addElementBefore(headElement, existingScript, newElementName); 258 } 259 260 /** 261 * Locates the head element under the root ("html") element, creating it if necessary, and adds the stylesheets to 262 * it. 263 * 264 * @param root 265 * element of document 266 * @param stylesheets 267 * to add to the document 268 */ 269 protected void addStylesheetsToHead(Element root, List<StylesheetLink> stylesheets) 270 { 271 int count = stylesheets.size(); 272 273 if (count == 0) 274 { 275 return; 276 } 277 278 // This only applies when the document is an HTML document. This may need to change in the 279 // future, perhaps configurable, to allow for html and xhtml and perhaps others. Does SVG 280 // use stylesheets? 281 282 String rootElementName = root.getName(); 283 284 // Not an html document, don't add anything. 285 if (!rootElementName.equals("html")) 286 { 287 return; 288 } 289 290 Element head = findOrCreateElement(root, "head", true); 291 292 // Create a temporary container element. 293 Element container = createTemporaryContainer(head, "style", "stylesheet-container"); 294 295 for (int i = 0; i < count; i++) 296 { 297 stylesheets.get(i).add(container); 298 } 299 300 container.pop(); 301 } 302 303 public void addModuleConfigurationCallback(ModuleConfigurationCallback callback) 304 { 305 assert callback != null; 306 moduleConfigurationCallbacks.add(callback); 307 } 308 309}