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         * <p>
189         * TBD: Security issues. Hackers can download .class files.
190         */
191    
192        public void service(IRequestCycle cycle) throws IOException
193        {
194            // If they were vended an asset in the past then it must be up-to date.
195            // Asset URIs change if the underlying file is modified.
196    
197            if (_request.getHeader("If-Modified-Since") != null)
198            {
199                _response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
200                return;
201            }
202    
203            String path = cycle.getParameter(PATH);
204            String md5 = cycle.getParameter(DIGEST);
205    
206            try
207            {
208                if (!_digestSource.getDigestForResource(path).equals(md5))
209                    throw new ApplicationRuntimeException(AssetMessages.md5Mismatch(path));
210    
211                URL resourceURL = _classResolver.getResource(path);
212    
213                if (resourceURL == null)
214                    throw new ApplicationRuntimeException(AssetMessages.noSuchResource(path));
215    
216                URLConnection resourceConnection = resourceURL.openConnection();
217    
218                writeAssetContent(cycle, path, resourceConnection);
219            }
220            catch (Throwable ex)
221            {
222                _exceptionReporter.reportRequestException(AssetMessages.exceptionReportTitle(path), ex);
223            }
224    
225        }
226    
227        /** @since 2.2 */
228    
229        private void writeAssetContent(IRequestCycle cycle, String resourcePath,
230                URLConnection resourceConnection) throws IOException
231        {
232            InputStream input = null;
233    
234            try
235            {
236                // Getting the content type and length is very dependant
237                // on support from the application server (represented
238                // here by the servletContext).
239    
240                String contentType = getMimeType(resourcePath);
241                int contentLength = resourceConnection.getContentLength();
242    
243                if (contentLength > 0)
244                    _response.setContentLength(contentLength);
245    
246                _response.setDateHeader("Last-Modified", _startupTime);
247                _response.setDateHeader("Expires", _expireTime);
248    
249                // Set the content type. If the servlet container doesn't
250                // provide it, try and guess it by the extension.
251    
252                if (contentType == null || contentType.length() == 0)
253                    contentType = getMimeType(resourcePath);
254    
255                OutputStream output = _response.getOutputStream(new ContentType(contentType));
256    
257                input = new BufferedInputStream(resourceConnection.getInputStream());
258    
259                byte[] buffer = new byte[BUFFER_SIZE];
260    
261                while (true)
262                {
263                    int bytesRead = input.read(buffer);
264    
265                    if (bytesRead < 0)
266                        break;
267    
268                    output.write(buffer, 0, bytesRead);
269                }
270    
271                input.close();
272                input = null;
273            }
274            finally
275            {
276                IOUtils.close(input);
277            }
278        }
279    
280        /** @since 4.0 */
281    
282        public void setExceptionReporter(RequestExceptionReporter exceptionReporter)
283        {
284            _exceptionReporter = exceptionReporter;
285        }
286    
287        /** @since 4.0 */
288        public void setLinkFactory(LinkFactory linkFactory)
289        {
290            _linkFactory = linkFactory;
291        }
292    
293        /** @since 4.0 */
294        public void setClassResolver(ClassResolver classResolver)
295        {
296            _classResolver = classResolver;
297        }
298    
299        /** @since 4.0 */
300        public void setContext(WebContext context)
301        {
302            _context = context;
303        }
304    
305        /** @since 4.0 */
306        public void setResponse(WebResponse response)
307        {
308            _response = response;
309        }
310    
311        /** @since 4.0 */
312        public void setDigestSource(ResourceDigestSource md5Source)
313        {
314            _digestSource = md5Source;
315        }
316    
317        /** @since 4.0 */
318        public void setRequest(WebRequest request)
319        {
320            _request = request;
321        }
322    }