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 final Location _location; 043 044 private final ResponseBuilder _builder; 045 046 // Lines that belong inside the onLoad event handler for the <body> tag. 047 private StringBuffer _initializationScript; 048 049 // Any other scripting desired 050 051 private StringBuffer _bodyScript; 052 053 // Contains text lines related to image initializations 054 055 private StringBuffer _imageInitializations; 056 057 /** 058 * Map of URLs to Strings (preloaded image references). 059 */ 060 061 private Map _imageMap; 062 063 /** 064 * List of included scripts. Values are Strings. 065 * 066 * @since 1.0.5 067 */ 068 069 private List _externalScripts; 070 071 private final IdAllocator _idAllocator; 072 073 private final String _preloadName; 074 075 private final Map _requires = new HashMap(); 076 077 public PageRenderSupportImpl(AssetFactory assetFactory, String namespace, 078 Location location, ResponseBuilder builder) 079 { 080 Defense.notNull(assetFactory, "assetService"); 081 082 _assetFactory = assetFactory; 083 _location = location; 084 _idAllocator = new IdAllocator(namespace); 085 _builder = builder; 086 087 _preloadName = (namespace.equals("") ? "tapestry." : namespace) + "preload"; 088 } 089 090 /** 091 * Returns the location, which may be used in error messages. In practical terms, this is the 092 * location of the {@link org.apache.tapestry.html.Body} component. 093 */ 094 095 public Location getLocation() 096 { 097 return _location; 098 } 099 100 public String getPreloadedImageReference(String URL) 101 { 102 return getPreloadedImageReference(null, URL); 103 } 104 105 public String getPreloadedImageReference(IComponent target, IAsset source) 106 { 107 return getPreloadedImageReference(target, source.buildURL()); 108 } 109 110 public String getPreloadedImageReference(IComponent target, String URL) 111 { 112 if (target != null 113 && !_builder.isImageInitializationAllowed(target)) 114 return URL; 115 116 if (_imageMap == null) 117 _imageMap = new HashMap(); 118 119 String reference = (String) _imageMap.get(URL); 120 121 if (reference == null) 122 { 123 int count = _imageMap.size(); 124 String varName = _preloadName + "[" + count + "]"; 125 reference = varName + ".src"; 126 127 if (_imageInitializations == null) 128 _imageInitializations = new StringBuffer(); 129 130 _imageInitializations.append(" "); 131 _imageInitializations.append(varName); 132 _imageInitializations.append(" = new Image();\n"); 133 _imageInitializations.append(" "); 134 _imageInitializations.append(reference); 135 _imageInitializations.append(" = \""); 136 _imageInitializations.append(URL); 137 _imageInitializations.append("\";\n"); 138 139 _imageMap.put(URL, reference); 140 } 141 142 return reference; 143 } 144 145 public void addBodyScript(String script) 146 { 147 addBodyScript(null, script); 148 } 149 150 public void addBodyScript(IComponent target, String script) 151 { 152 if (!_builder.isBodyScriptAllowed(target)) 153 return; 154 155 String val = stripDuplicateIncludes(script); 156 157 if (_bodyScript == null) 158 _bodyScript = new StringBuffer(val.length()); 159 160 _bodyScript.append("\n").append(val); 161 } 162 163 /** 164 * {@inheritDoc} 165 */ 166 public boolean isBodyScriptAllowed(IComponent target) 167 { 168 return _builder.isBodyScriptAllowed(target); 169 } 170 171 /** 172 * {@inheritDoc} 173 */ 174 public boolean isExternalScriptAllowed(IComponent target) 175 { 176 return _builder.isExternalScriptAllowed(target); 177 } 178 179 /** 180 * {@inheritDoc} 181 */ 182 public boolean isInitializationScriptAllowed(IComponent target) 183 { 184 return _builder.isInitializationScriptAllowed(target); 185 } 186 187 public void addInitializationScript(String script) 188 { 189 addInitializationScript(null, script); 190 } 191 192 public void addInitializationScript(IComponent target, String script) 193 { 194 if (!_builder.isInitializationScriptAllowed(target)) 195 return; 196 197 String val = stripDuplicateIncludes(script); 198 199 if (_initializationScript == null) 200 _initializationScript = new StringBuffer(val.length() + 1); 201 202 _initializationScript.append("\n").append(val); 203 } 204 205 /** 206 * Provides a mechanism to strip out duplicate dojo.require calls made in script 207 * templates in order to reduce amount of redundant javascript written to client. 208 * 209 * @param input The incoming script string to check for requires. 210 * @return The input string stripped of all known dojo.require calls, if any. 211 */ 212 String stripDuplicateIncludes(String input) 213 { 214 String[] lines = StringUtils.splitPreserveAllTokens(input, ';'); 215 216 if (lines == null || lines.length < 1) 217 return input; 218 219 String ret = input; 220 221 for (int i=0; i < lines.length; i++) { 222 if (lines[i].indexOf("dojo.require") < 0) 223 continue; 224 225 String line = StringUtils.stripToEmpty(lines[i]); 226 227 if (_requires.containsKey(line)) { 228 ret = StringUtils.replaceOnce(ret, line+";", ""); 229 } else { 230 _requires.put(line, "t"); 231 } 232 } 233 234 return StringUtils.stripToEmpty(ret.trim()); 235 } 236 237 public void addExternalScript(Resource scriptLocation) 238 { 239 addExternalScript(null, scriptLocation); 240 } 241 242 public void addExternalScript(IComponent target, Resource scriptLocation) 243 { 244 if (!_builder.isExternalScriptAllowed(target)) 245 return; 246 247 if (_externalScripts == null) 248 _externalScripts = new ArrayList(); 249 250 if (_externalScripts.contains(scriptLocation)) 251 return; 252 253 // Record the Resource so we don't include it twice. 254 255 _externalScripts.add(scriptLocation); 256 } 257 258 public String getUniqueString(String baseValue) 259 { 260 return _idAllocator.allocateId(baseValue); 261 } 262 263 private void writeExternalScripts(IMarkupWriter writer, IRequestCycle cycle) 264 { 265 int count = Tapestry.size(_externalScripts); 266 for (int i = 0; i < count; i++) 267 { 268 Resource scriptLocation = (Resource) _externalScripts.get(i); 269 270 IAsset asset = _assetFactory.createAsset(scriptLocation, null); 271 272 String url = asset.buildURL(); 273 274 // Note: important to use begin(), not beginEmpty(), because browser don't 275 // interpret <script .../> properly. 276 277 _builder.writeExternalScript(writer, url, cycle); 278 } 279 } 280 281 /** 282 * Writes a single large JavaScript block containing: 283 * <ul> 284 * <li>Any image initializations (via {@link #getPreloadedImageReference(IComponent, String)}). 285 * <li>Any included scripts (via {@link #addExternalScript(Resource)}). 286 * <li>Any contributions (via {@link #addBodyScript(String)}). 287 * </ul> 288 * 289 * @see #writeInitializationScript(IMarkupWriter) 290 */ 291 292 public void writeBodyScript(IMarkupWriter writer, IRequestCycle cycle) 293 { 294 if (!Tapestry.isEmpty(_externalScripts)) 295 writeExternalScripts(writer, cycle); 296 297 if (!(any(_bodyScript) || any(_imageInitializations))) 298 return; 299 300 _builder.beginBodyScript(writer, cycle); 301 302 if (any(_imageInitializations)) 303 { 304 _builder.writeImageInitializations(writer, StringUtils.stripToEmpty(_imageInitializations.toString()) 305 , _preloadName, cycle); 306 } 307 308 if (any(_bodyScript)) 309 { 310 _builder.writeBodyScript(writer, StringUtils.stripToEmpty(_bodyScript.toString()) 311 , 312 cycle); 313 } 314 315 _builder.endBodyScript(writer, cycle); 316 } 317 318 /** 319 * Writes any image initializations; this should be invoked at the end of the render, after all 320 * the related HTML will have already been streamed to the client and parsed by the web browser. 321 * Earlier versions of Tapestry uses a <code>window.onload</code> event handler. 322 */ 323 324 public void writeInitializationScript(IMarkupWriter writer) 325 { 326 if (!any(_initializationScript)) 327 return; 328 329 _builder.writeInitializationScript(writer, StringUtils.stripToEmpty(_initializationScript.toString())); 330 } 331 332 private boolean any(StringBuffer buffer) 333 { 334 return buffer != null && buffer.length() > 0; 335 } 336 }