001    // Copyright 2004, 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.asset;
016    
017    import java.io.BufferedInputStream;
018    import java.io.IOException;
019    import java.io.InputStream;
020    import java.io.OutputStream;
021    import java.net.URL;
022    import java.net.URLConnection;
023    import java.util.HashMap;
024    import java.util.Map;
025    
026    import javax.servlet.http.HttpServletResponse;
027    
028    import org.apache.hivemind.ApplicationRuntimeException;
029    import org.apache.hivemind.ClassResolver;
030    import org.apache.hivemind.util.Defense;
031    import org.apache.hivemind.util.IOUtils;
032    import org.apache.tapestry.IRequestCycle;
033    import org.apache.tapestry.Tapestry;
034    import org.apache.tapestry.engine.IEngineService;
035    import org.apache.tapestry.engine.ILink;
036    import org.apache.tapestry.error.RequestExceptionReporter;
037    import org.apache.tapestry.services.LinkFactory;
038    import org.apache.tapestry.services.ServiceConstants;
039    import org.apache.tapestry.util.ContentType;
040    import org.apache.tapestry.web.WebContext;
041    import org.apache.tapestry.web.WebRequest;
042    import org.apache.tapestry.web.WebResponse;
043    
044    /**
045     * A service for building URLs to and accessing {@link org.apache.tapestry.IAsset}s. Most of the
046     * work is deferred to the {@link org.apache.tapestry.IAsset}instance.
047     * <p>
048     * The retrieval part is directly linked to {@link PrivateAsset}. The service responds to a URL
049     * that encodes the path of a resource within the classpath. The {@link #service(IRequestCycle)}
050     * method reads the resource and streams it out.
051     * <p>
052     * TBD: Security issues. Should only be able to retrieve a resource that was previously registerred
053     * in some way ... otherwise, hackers will be able to suck out the .class files of the application!
054     * 
055     * @author Howard Lewis Ship
056     */
057    
058    public class AssetService implements IEngineService
059    {
060    
061        /** @since 4.0 */
062        private ClassResolver _classResolver;
063    
064        /** @since 4.0 */
065        private LinkFactory _linkFactory;
066    
067        /** @since 4.0 */
068        private WebContext _context;
069    
070        /** @since 4.0 */
071    
072        private WebRequest _request;
073    
074        /** @since 4.0 */
075        private WebResponse _response;
076    
077        /** @since 4.0 */
078        private ResourceDigestSource _digestSource;
079    
080        /**
081         * Defaults MIME types, by extension, used when the servlet container doesn't provide MIME
082         * types. ServletExec Debugger, for example, fails to provide these.
083         */
084    
085        private final static Map _mimeTypes;
086    
087        static
088        {
089            _mimeTypes = new HashMap(17);
090            _mimeTypes.put("css", "text/css");
091            _mimeTypes.put("gif", "image/gif");
092            _mimeTypes.put("jpg", "image/jpeg");
093            _mimeTypes.put("jpeg", "image/jpeg");
094            _mimeTypes.put("htm", "text/html");
095            _mimeTypes.put("html", "text/html");
096        }
097    
098        private static final int BUFFER_SIZE = 10240;
099    
100        /**
101         * Startup time for this service; used to set the Last-Modified response header.
102         * 
103         * @since 4.0
104         */
105    
106        private final long _startupTime = System.currentTimeMillis();
107    
108        /**
109         * Time vended assets expire. Since a change in asset content is a change in asset URI, we want
110         * them to not expire ... but a year will do.
111         */
112    
113        private final long _expireTime = _startupTime + 365 * 24 * 60 * 60 * 1000;
114    
115        /** @since 4.0 */
116    
117        private RequestExceptionReporter _exceptionReporter;
118    
119        /**
120         * Query parameter that stores the path to the resource (with a leading slash).
121         * 
122         * @since 4.0
123         */
124    
125        public static final String PATH = "path";
126    
127        /**
128         * Query parameter that stores the digest for the file; this is used to authenticate that the
129         * client is allowed to access the file.
130         * 
131         * @since 4.0
132         */
133    
134        public static final String DIGEST = "digest";
135    
136        /**
137         * Builds a {@link ILink}for a {@link PrivateAsset}.
138         * <p>
139         * A single parameter is expected, the resource path of the asset (which is expected to start
140         * with a leading slash).
141         */
142    
143        public ILink getLink(IRequestCycle cycle, boolean post, Object parameter)
144        {
145            Defense.isAssignable(parameter, String.class, "parameter");
146    
147            String path = (String) parameter;
148    
149            String digest = _digestSource.getDigestForResource(path);
150    
151            Map parameters = new HashMap();
152    
153            parameters.put(ServiceConstants.SERVICE, getName());
154            parameters.put(PATH, path);
155            parameters.put(DIGEST, digest);
156    
157            // Service is stateless, which is the exception to the rule.
158    
159            return _linkFactory.constructLink(cycle, post, parameters, false);
160        }
161    
162        public String getName()
163        {
164            return Tapestry.ASSET_SERVICE;
165        }
166    
167        private String getMimeType(String path)
168        {
169            String result = _context.getMimeType(path);
170    
171            if (result == null)
172            {
173                int dotx = path.lastIndexOf('.');
174                String key = path.substring(dotx + 1).toLowerCase();
175    
176                result = (String) _mimeTypes.get(key);
177    
178                if (result == null)
179                    result = "text/plain";
180            }
181    
182            return result;
183        }
184    
185        /**
186         * Retrieves a resource from the classpath and returns it to the client in a binary output
187         * stream.
188         */
189    
190        public void service(IRequestCycle cycle) throws IOException
191        {
192            String path = cycle.getParameter(PATH);
193            String md5Digest = cycle.getParameter(DIGEST);
194    
195            try
196            {
197                if (!_digestSource.getDigestForResource(path).equals(md5Digest))
198                {
199                    _response.sendError(HttpServletResponse.SC_FORBIDDEN, AssetMessages
200                            .md5Mismatch(path));
201                    return;
202                }
203    
204                // If they were vended an asset in the past then it must be up-to date.
205                // Asset URIs change if the underlying file is modified.
206    
207                if (_request.getHeader("If-Modified-Since") != null)
208                {
209                    _response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
210                    return;
211                }
212    
213                URL resourceURL = _classResolver.getResource(path);
214    
215                if (resourceURL == null)
216                    throw new ApplicationRuntimeException(AssetMessages.noSuchResource(path));
217    
218                URLConnection resourceConnection = resourceURL.openConnection();
219    
220                writeAssetContent(cycle, path, resourceConnection);
221            }
222            catch (Throwable ex)
223            {
224                _exceptionReporter.reportRequestException(AssetMessages.exceptionReportTitle(path), ex);
225            }
226    
227        }
228    
229        /** @since 2.2 */
230    
231        private void writeAssetContent(IRequestCycle cycle, String resourcePath,
232                URLConnection resourceConnection) throws IOException
233        {
234            InputStream input = null;
235    
236            try
237            {
238                // Getting the content type and length is very dependant
239                // on support from the application server (represented
240                // here by the servletContext).
241    
242                String contentType = getMimeType(resourcePath);
243                int contentLength = resourceConnection.getContentLength();
244    
245                if (contentLength > 0)
246                    _response.setContentLength(contentLength);
247    
248                _response.setDateHeader("Last-Modified", _startupTime);
249                _response.setDateHeader("Expires", _expireTime);
250    
251                // Set the content type. If the servlet container doesn't
252                // provide it, try and guess it by the extension.
253    
254                if (contentType == null || contentType.length() == 0)
255                    contentType = getMimeType(resourcePath);
256    
257                OutputStream output = _response.getOutputStream(new ContentType(contentType));
258    
259                input = new BufferedInputStream(resourceConnection.getInputStream());
260    
261                byte[] buffer = new byte[BUFFER_SIZE];
262    
263                while (true)
264                {
265                    int bytesRead = input.read(buffer);
266    
267                    if (bytesRead < 0)
268                        break;
269    
270                    output.write(buffer, 0, bytesRead);
271                }
272    
273                input.close();
274                input = null;
275            }
276            finally
277            {
278                IOUtils.close(input);
279            }
280        }
281    
282        /** @since 4.0 */
283    
284        public void setExceptionReporter(RequestExceptionReporter exceptionReporter)
285        {
286            _exceptionReporter = exceptionReporter;
287        }
288    
289        /** @since 4.0 */
290        public void setLinkFactory(LinkFactory linkFactory)
291        {
292            _linkFactory = linkFactory;
293        }
294    
295        /** @since 4.0 */
296        public void setClassResolver(ClassResolver classResolver)
297        {
298            _classResolver = classResolver;
299        }
300    
301        /** @since 4.0 */
302        public void setContext(WebContext context)
303        {
304            _context = context;
305        }
306    
307        /** @since 4.0 */
308        public void setResponse(WebResponse response)
309        {
310            _response = response;
311        }
312    
313        /** @since 4.0 */
314        public void setDigestSource(ResourceDigestSource md5Source)
315        {
316            _digestSource = md5Source;
317        }
318    
319        /** @since 4.0 */
320        public void setRequest(WebRequest request)
321        {
322            _request = request;
323        }
324    }