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