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}&nbsp;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    }