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