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 }