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 }