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