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
015package org.apache.tapestry.asset;
016
017import java.io.BufferedInputStream;
018import java.io.IOException;
019import java.io.InputStream;
020import java.io.OutputStream;
021import java.net.URL;
022import java.net.URLConnection;
023import java.util.HashMap;
024import java.util.Map;
025
026import javax.servlet.http.HttpServletResponse;
027
028import org.apache.hivemind.ApplicationRuntimeException;
029import org.apache.hivemind.ClassResolver;
030import org.apache.hivemind.util.Defense;
031import org.apache.hivemind.util.IOUtils;
032import org.apache.tapestry.IRequestCycle;
033import org.apache.tapestry.Tapestry;
034import org.apache.tapestry.engine.IEngineService;
035import org.apache.tapestry.engine.ILink;
036import org.apache.tapestry.error.RequestExceptionReporter;
037import org.apache.tapestry.services.LinkFactory;
038import org.apache.tapestry.services.ServiceConstants;
039import org.apache.tapestry.util.ContentType;
040import org.apache.tapestry.web.WebContext;
041import org.apache.tapestry.web.WebRequest;
042import 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
058public 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}