001 // Copyright 2005 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.tapestry.util; 016 017 import org.apache.commons.lang.StringUtils; 018 import org.apache.hivemind.Locatable; 019 import org.apache.hivemind.Location; 020 import org.apache.hivemind.Resource; 021 import org.apache.hivemind.util.Defense; 022 import org.apache.tapestry.*; 023 import org.apache.tapestry.asset.AssetFactory; 024 import org.apache.tapestry.services.ResponseBuilder; 025 026 import java.util.ArrayList; 027 import java.util.HashMap; 028 import java.util.List; 029 import java.util.Map; 030 031 /** 032 * Implementation of {@link org.apache.tapestry.PageRenderSupport}. The 033 * {@link org.apache.tapestry.html.Body} component uses an instance of this class. 034 * 035 * @author Howard M. Lewis Ship 036 * @since 4.0 037 */ 038 public class PageRenderSupportImpl implements Locatable, PageRenderSupport 039 { 040 private final AssetFactory _assetFactory; 041 042 private Location _location; 043 044 private final ResponseBuilder _builder; 045 046 // Lines that belong inside the onLoad event handler for the <body> tag. 047 048 private StringBuffer _initializationScript; 049 050 // Used by addScriptAfterInitialization 051 052 private StringBuffer _postInitializationScript; 053 054 // Any other scripting desired 055 056 private StringBuffer _bodyScript; 057 058 // Contains text lines related to image initializations 059 060 private StringBuffer _imageInitializations; 061 062 /** 063 * Map of URLs to Strings (preloaded image references). 064 */ 065 066 private Map _imageMap; 067 068 /** 069 * List of included scripts. Values are Strings. 070 * 071 * @since 1.0.5 072 */ 073 074 private List _externalScripts; 075 076 private final IdAllocator _idAllocator; 077 078 private final String _preloadName; 079 080 private final Map _requires = new HashMap(); 081 082 private IRequestCycle _cycle; 083 084 /** 085 * Creates a new instance bound to the specific location. 086 * 087 * @param assetFactory 088 * Used to generate asset urls. 089 * @param namespace 090 * Namespace that javascript / portlet related items should be in. 091 * @param location 092 * Location of what is primarily the {@link org.apache.tapestry.html.Body} component. 093 * @param builder 094 * The response delegate. 095 * 096 * @deprecated To be removed in 4.1.2 - use the 097 * new {@link #PageRenderSupportImpl(org.apache.tapestry.asset.AssetFactory, String, org.apache.tapestry.services.ResponseBuilder, org.apache.tapestry.IRequestCycle)} 098 * constructor instead. 099 */ 100 public PageRenderSupportImpl(AssetFactory assetFactory, String namespace, 101 Location location, ResponseBuilder builder) 102 { 103 Defense.notNull(assetFactory, "assetService"); 104 105 _assetFactory = assetFactory; 106 _location = location; 107 _idAllocator = new IdAllocator(namespace); 108 _builder = builder; 109 110 _preloadName = (namespace.equals("") ? "tapestry." : namespace) + "preload"; 111 } 112 113 public PageRenderSupportImpl(AssetFactory assetFactory, String namespace, 114 ResponseBuilder builder, IRequestCycle cycle) 115 { 116 Defense.notNull(assetFactory, "assetService"); 117 118 _assetFactory = assetFactory; 119 _idAllocator = new IdAllocator(namespace); 120 _builder = builder; 121 _cycle = cycle; 122 123 _preloadName = (namespace.equals("") ? "tapestry." : namespace) + "preload"; 124 } 125 126 /** 127 * Returns the location, which may be used in error messages. In practical terms, this is the 128 * location of the {@link org.apache.tapestry.html.Body} component. 129 */ 130 131 public Location getLocation() 132 { 133 if (_location != null) 134 { 135 return _location; 136 } 137 138 if (_cycle != null) 139 { 140 IRender render = _cycle.renderStackPeek(); 141 142 if (render != null && IComponent.class.isInstance(render)) 143 { 144 return ((IComponent)render).getLocation(); 145 } 146 } 147 148 return null; 149 } 150 151 public String getPreloadedImageReference(String URL) 152 { 153 return getPreloadedImageReference(null, URL); 154 } 155 156 public String getPreloadedImageReference(IComponent target, IAsset source) 157 { 158 return getPreloadedImageReference(target, source.buildURL()); 159 } 160 161 public String getPreloadedImageReference(IComponent target, String URL) 162 { 163 if (target != null 164 && !_builder.isImageInitializationAllowed(target)) 165 return URL; 166 167 if (_imageMap == null) 168 _imageMap = new HashMap(); 169 170 String reference = (String) _imageMap.get(URL); 171 172 if (reference == null) 173 { 174 int count = _imageMap.size(); 175 String varName = _preloadName + "[" + count + "]"; 176 reference = varName + ".src"; 177 178 if (_imageInitializations == null) 179 _imageInitializations = new StringBuffer(); 180 181 _imageInitializations.append(" "); 182 _imageInitializations.append(varName); 183 _imageInitializations.append(" = new Image();\n"); 184 _imageInitializations.append(" "); 185 _imageInitializations.append(reference); 186 _imageInitializations.append(" = \""); 187 _imageInitializations.append(URL); 188 _imageInitializations.append("\";\n"); 189 190 _imageMap.put(URL, reference); 191 } 192 193 return reference; 194 } 195 196 public void addBodyScript(String script) 197 { 198 addBodyScript(null, script); 199 } 200 201 public void addBodyScript(IComponent target, String script) 202 { 203 if (!_builder.isBodyScriptAllowed(target)) 204 return; 205 206 String val = stripDuplicateIncludes(script); 207 208 if (_bodyScript == null) 209 _bodyScript = new StringBuffer(val.length()); 210 211 _bodyScript.append("\n").append(val); 212 } 213 214 /** 215 * {@inheritDoc} 216 */ 217 public boolean isBodyScriptAllowed(IComponent target) 218 { 219 return _builder.isBodyScriptAllowed(target); 220 } 221 222 /** 223 * {@inheritDoc} 224 */ 225 public boolean isExternalScriptAllowed(IComponent target) 226 { 227 return _builder.isExternalScriptAllowed(target); 228 } 229 230 /** 231 * {@inheritDoc} 232 */ 233 public boolean isInitializationScriptAllowed(IComponent target) 234 { 235 return _builder.isInitializationScriptAllowed(target); 236 } 237 238 public void addInitializationScript(String script) 239 { 240 addInitializationScript(null, script); 241 } 242 243 public void addInitializationScript(IComponent target, String script) 244 { 245 if (!_builder.isInitializationScriptAllowed(target)) 246 return; 247 248 String val = stripDuplicateIncludes(script); 249 250 if (_initializationScript == null) 251 _initializationScript = new StringBuffer(val.length() + 1); 252 253 _initializationScript.append("\n").append(val); 254 } 255 256 public void addScriptAfterInitialization(IComponent target, String script) 257 { 258 if (!_builder.isInitializationScriptAllowed(target)) 259 return; 260 261 String strippedScript = stripDuplicateIncludes(script); 262 263 if (_postInitializationScript == null) 264 _postInitializationScript = new StringBuffer(strippedScript.length() + 1); 265 266 _postInitializationScript.append("\n").append(strippedScript); 267 } 268 269 /** 270 * Provides a mechanism to strip out duplicate dojo.require calls made in script 271 * templates in order to reduce amount of redundant javascript written to client. 272 * 273 * @param input The incoming script string to check for requires. 274 * @return The input string stripped of all known dojo.require calls, if any. 275 */ 276 String stripDuplicateIncludes(String input) 277 { 278 String[] lines = StringUtils.splitPreserveAllTokens(input, ';'); 279 280 if (lines == null || lines.length < 1) 281 return input; 282 283 String ret = input; 284 285 for (int i=0; i < lines.length; i++) 286 { 287 if (lines[i].indexOf("dojo.require") < 0) 288 continue; 289 290 String line = StringUtils.stripToEmpty(lines[i]); 291 292 if (_requires.containsKey(line)) 293 { 294 ret = StringUtils.replaceOnce(ret, line+";", ""); 295 } else 296 { 297 _requires.put(line, "t"); 298 } 299 } 300 301 return StringUtils.stripToEmpty(ret.trim()); 302 } 303 304 public void addExternalScript(Resource scriptLocation) 305 { 306 addExternalScript(null, scriptLocation); 307 } 308 309 public void addExternalScript(IComponent target, Resource scriptLocation) 310 { 311 if (!_builder.isExternalScriptAllowed(target)) 312 return; 313 314 if (_externalScripts == null) 315 _externalScripts = new ArrayList(); 316 317 if (_externalScripts.contains(scriptLocation)) 318 return; 319 320 // Record the Resource so we don't include it twice. 321 322 _externalScripts.add(scriptLocation); 323 } 324 325 public String getUniqueString(String baseValue) 326 { 327 return _idAllocator.allocateId(baseValue); 328 } 329 330 private void writeExternalScripts(IMarkupWriter writer, IRequestCycle cycle) 331 { 332 int count = Tapestry.size(_externalScripts); 333 for (int i = 0; i < count; i++) 334 { 335 Resource scriptLocation = (Resource) _externalScripts.get(i); 336 337 IAsset asset = _assetFactory.createAsset(scriptLocation, null); 338 339 String url = asset.buildURL(); 340 341 // Note: important to use begin(), not beginEmpty(), because browser don't 342 // interpret <script .../> properly. 343 344 _builder.writeExternalScript(writer, url, cycle); 345 } 346 } 347 348 /** 349 * Writes a single large JavaScript block containing: 350 * <ul> 351 * <li>Any image initializations (via {@link #getPreloadedImageReference(IComponent, String)}). 352 * <li>Any included scripts (via {@link #addExternalScript(Resource)}). 353 * <li>Any contributions (via {@link #addBodyScript(String)}). 354 * </ul> 355 * 356 * @see #writeInitializationScript(IMarkupWriter) 357 * @param writer 358 * The markup writer to use. 359 * @param cycle 360 * The current request. 361 */ 362 363 public void writeBodyScript(IMarkupWriter writer, IRequestCycle cycle) 364 { 365 if (!Tapestry.isEmpty(_externalScripts)) 366 writeExternalScripts(writer, cycle); 367 368 if (!(any(_bodyScript) || any(_imageInitializations))) 369 return; 370 371 _builder.beginBodyScript(writer, cycle); 372 373 if (any(_imageInitializations)) 374 { 375 _builder.writeImageInitializations(writer, 376 StringUtils.stripToEmpty(_imageInitializations.toString()), 377 _preloadName, 378 cycle); 379 } 380 381 if (any(_bodyScript)) 382 { 383 _builder.writeBodyScript(writer, StringUtils.stripToEmpty(_bodyScript.toString()), cycle); 384 } 385 386 _builder.endBodyScript(writer, cycle); 387 } 388 389 /** 390 * Writes any image initializations; this should be invoked at the end of the render, after all 391 * the related HTML will have already been streamed to the client and parsed by the web browser. 392 * Earlier versions of Tapestry uses a <code>window.onload</code> event handler. 393 * 394 * @param writer 395 * The markup writer to use. 396 */ 397 398 public void writeInitializationScript(IMarkupWriter writer) 399 { 400 if (!any(_initializationScript) && !any(_postInitializationScript)) 401 return; 402 403 String script = getContent(_initializationScript) + getContent(_postInitializationScript); 404 405 _builder.writeInitializationScript(writer, StringUtils.stripToEmpty(script)); 406 } 407 408 public static String getContent(StringBuffer buffer) 409 { 410 if (buffer == null || buffer.length() < 1) 411 return ""; 412 413 return buffer.toString(); 414 } 415 416 private boolean any(StringBuffer buffer) 417 { 418 return buffer != null && buffer.length() > 0; 419 } 420 }