001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.reading;
018:
019: import org.apache.avalon.framework.configuration.Configurable;
020: import org.apache.avalon.framework.configuration.Configuration;
021: import org.apache.avalon.framework.configuration.ConfigurationException;
022: import org.apache.avalon.framework.parameters.ParameterException;
023: import org.apache.avalon.framework.parameters.Parameters;
024:
025: import org.apache.cocoon.ProcessingException;
026: import org.apache.cocoon.caching.CacheableProcessingComponent;
027: import org.apache.cocoon.components.source.SourceUtil;
028: import org.apache.cocoon.environment.Context;
029: import org.apache.cocoon.environment.ObjectModelHelper;
030: import org.apache.cocoon.environment.Request;
031: import org.apache.cocoon.environment.Response;
032: import org.apache.cocoon.environment.SourceResolver;
033: import org.apache.cocoon.environment.http.HttpResponse;
034: import org.apache.cocoon.util.ByteRange;
035:
036: import org.apache.excalibur.source.Source;
037: import org.apache.excalibur.source.SourceException;
038: import org.apache.excalibur.source.SourceValidity;
039: import org.xml.sax.SAXException;
040:
041: import java.io.IOException;
042: import java.io.InputStream;
043: import java.io.Serializable;
044: import java.util.HashMap;
045: import java.util.Map;
046:
047: /**
048: * The <code>ResourceReader</code> component is used to serve binary data
049: * in a sitemap pipeline. It makes use of HTTP Headers to determine if
050: * the requested resource should be written to the <code>OutputStream</code>
051: * or if it can signal that it hasn't changed.
052: *
053: * <p>Configuration:
054: * <dl>
055: * <dt><expires></dt>
056: * <dd>This parameter is optional. When specified it determines how long
057: * in miliseconds the resources can be cached by any proxy or browser
058: * between Cocoon and the requesting visitor. Defaults to -1.
059: * </dd>
060: * <dt><quick-modified-test></dt>
061: * <dd>This parameter is optional. This boolean parameter controls the
062: * last modified test. If set to true (default is false), only the
063: * last modified of the current source is tested, but not if the
064: * same source is used as last time
065: * (see http://marc.theaimsgroup.com/?l=xml-cocoon-dev&m=102921894301915 )
066: * </dd>
067: * <dt><byte-ranges></dt>
068: * <dd>This parameter is optional. This boolean parameter controls whether
069: * Cocoon should support byterange requests (to allow clients to resume
070: * broken/interrupted downloads).
071: * Defaults to true.
072: * </dl>
073: *
074: * <p>Default configuration:
075: * <pre>
076: * <expires>-1</expires>
077: * <quick-modified-test>false</quick-modified-test>
078: * <byte-ranges>true</byte-ranges>
079: * </pre>
080: *
081: * <p>In addition to reader configuration, above parameters can be passed
082: * to the reader at the time when it is used.
083: *
084: * @author <a href="mailto:Giacomo.Pati@pwr.ch">Giacomo Pati</a>
085: * @author <a href="mailto:tcurdt@apache.org">Torsten Curdt</a>
086: * @author <a href="mailto:cziegeler@apache.org">Carsten Ziegeler</a>
087: * @version CVS $Id: ResourceReader.java 433543 2006-08-22 06:22:54Z crossley $
088: */
089: public class ResourceReader extends AbstractReader implements
090: CacheableProcessingComponent, Configurable {
091:
092: /**
093: * The list of generated documents
094: */
095: private static final Map documents = new HashMap();
096:
097: protected long configuredExpires;
098: protected boolean configuredQuickTest;
099: protected int configuredBufferSize;
100: protected boolean configuredByteRanges;
101:
102: protected long expires;
103: protected boolean quickTest;
104: protected int bufferSize;
105: protected boolean byteRanges;
106:
107: protected Response response;
108: protected Request request;
109: protected Source inputSource;
110:
111: /**
112: * Read reader configuration
113: */
114: public void configure(Configuration configuration)
115: throws ConfigurationException {
116: // VG Parameters are deprecated as of 2.2.0-Dev/2.1.6-Dev
117: final Parameters parameters = Parameters
118: .fromConfiguration(configuration);
119: this .configuredExpires = parameters.getParameterAsLong(
120: "expires", -1);
121: this .configuredQuickTest = parameters.getParameterAsBoolean(
122: "quick-modified-test", false);
123: this .configuredBufferSize = parameters.getParameterAsInteger(
124: "buffer-size", 8192);
125: this .configuredByteRanges = parameters.getParameterAsBoolean(
126: "byte-ranges", true);
127:
128: // Configuration has precedence over parameters.
129: this .configuredExpires = configuration.getChild("expires")
130: .getValueAsLong(configuredExpires);
131: this .configuredQuickTest = configuration.getChild(
132: "quick-modified-test").getValueAsBoolean(
133: configuredQuickTest);
134: this .configuredBufferSize = configuration.getChild(
135: "buffer-size").getValueAsInteger(configuredBufferSize);
136: this .configuredByteRanges = configuration.getChild(
137: "byte-ranges").getValueAsBoolean(configuredByteRanges);
138: }
139:
140: /* (non-Javadoc)
141: * @see org.apache.avalon.framework.parameters.Parameterizable#parameterize(Parameters)
142: */
143: public void parameterize(Parameters parameters)
144: throws ParameterException {
145: }
146:
147: /**
148: * Setup the reader.
149: * The resource is opened to get an <code>InputStream</code>,
150: * the length and the last modification date
151: */
152: public void setup(SourceResolver resolver, Map objectModel,
153: String src, Parameters par) throws ProcessingException,
154: SAXException, IOException {
155: super .setup(resolver, objectModel, src, par);
156:
157: this .request = ObjectModelHelper.getRequest(objectModel);
158: this .response = ObjectModelHelper.getResponse(objectModel);
159:
160: this .expires = par.getParameterAsLong("expires",
161: this .configuredExpires);
162: this .quickTest = par.getParameterAsBoolean(
163: "quick-modified-test", this .configuredQuickTest);
164: this .bufferSize = par.getParameterAsInteger("buffer-size",
165: this .configuredBufferSize);
166: this .byteRanges = par.getParameterAsBoolean("byte-ranges",
167: this .configuredByteRanges);
168:
169: try {
170: this .inputSource = resolver.resolveURI(src);
171: } catch (SourceException e) {
172: throw SourceUtil.handle("Error during resolving of '" + src
173: + "'.", e);
174: }
175: setupHeaders();
176: }
177:
178: /**
179: * Setup the response headers: Accept-Ranges, Expires, Last-Modified
180: */
181: protected void setupHeaders() {
182: // Tell the client whether we support byte range requests or not
183: if (byteRanges) {
184: response.setHeader("Accept-Ranges", "bytes");
185: } else {
186: response.setHeader("Accept-Ranges", "none");
187: }
188:
189: if (expires > 0) {
190: response.setDateHeader("Expires", System
191: .currentTimeMillis()
192: + expires);
193: } else if (expires == 0) {
194: response.setDateHeader("Expires", 0);
195: }
196:
197: long lastModified = getLastModified();
198: if (lastModified > 0) {
199: response.setDateHeader("Last-Modified", lastModified);
200: }
201: }
202:
203: /**
204: * Recyclable
205: */
206: public void recycle() {
207: this .request = null;
208: this .response = null;
209: if (this .inputSource != null) {
210: super .resolver.release(this .inputSource);
211: this .inputSource = null;
212: }
213: super .recycle();
214: }
215:
216: /**
217: * @return True if byte ranges support is enabled and request has range header.
218: */
219: protected boolean hasRanges() {
220: return this .byteRanges
221: && this .request.getHeader("Range") != null;
222: }
223:
224: /**
225: * Generate the unique key.
226: * This key must be unique inside the space of this component.
227: *
228: * @return The generated key hashes the src
229: */
230: public Serializable getKey() {
231: return inputSource.getURI();
232: }
233:
234: /**
235: * Generate the validity object.
236: *
237: * @return The generated validity object or <code>null</code> if the
238: * component is currently not cacheable.
239: */
240: public SourceValidity getValidity() {
241: if (hasRanges()) {
242: // This is a byte range request so we can't use the cache, return null.
243: return null;
244: } else {
245: return inputSource.getValidity();
246: }
247: }
248:
249: /**
250: * @return the time the read source was last modified or 0 if it is not
251: * possible to detect
252: */
253: public long getLastModified() {
254: if (hasRanges()) {
255: // This is a byte range request so we can't use the cache, return null.
256: return 0;
257: }
258:
259: if (quickTest) {
260: return inputSource.getLastModified();
261: }
262:
263: final String systemId = (String) documents.get(request
264: .getRequestURI());
265: if (systemId == null || inputSource.getURI().equals(systemId)) {
266: return inputSource.getLastModified();
267: }
268:
269: documents.remove(request.getRequestURI());
270: return 0;
271: }
272:
273: protected void processStream(InputStream inputStream)
274: throws IOException, ProcessingException {
275: byte[] buffer = new byte[bufferSize];
276: int length = -1;
277:
278: String ranges = request.getHeader("Range");
279:
280: ByteRange byteRange;
281: if (byteRanges && ranges != null) {
282: try {
283: ranges = ranges.substring(ranges.indexOf('=') + 1);
284: byteRange = new ByteRange(ranges);
285: } catch (NumberFormatException e) {
286: byteRange = null;
287:
288: // TC: Hm.. why don't we have setStatus in the Response interface ?
289: if (response instanceof HttpResponse) {
290: // Respond with status 416 (Request range not satisfiable)
291: ((HttpResponse) response).setStatus(416);
292: if (getLogger().isDebugEnabled()) {
293: getLogger().debug(
294: "malformed byte range header ["
295: + String.valueOf(ranges) + "]");
296: }
297: }
298: }
299: } else {
300: byteRange = null;
301: }
302:
303: long contentLength = inputSource.getContentLength();
304:
305: if (byteRange != null) {
306: String entityLength;
307: String entityRange;
308: if (contentLength != -1) {
309: entityLength = "" + contentLength;
310: entityRange = byteRange.intersection(
311: new ByteRange(0, contentLength)).toString();
312: } else {
313: entityLength = "*";
314: entityRange = byteRange.toString();
315: }
316:
317: response.setHeader("Content-Range", entityRange + "/"
318: + entityLength);
319: if (response instanceof HttpResponse) {
320: // Response with status 206 (Partial content)
321: ((HttpResponse) response).setStatus(206);
322: }
323:
324: int pos = 0;
325: int posEnd;
326: while ((length = inputStream.read(buffer)) > -1) {
327: posEnd = pos + length - 1;
328: ByteRange intersection = byteRange
329: .intersection(new ByteRange(pos, posEnd));
330: if (intersection != null) {
331: out.write(buffer, (int) intersection.getStart()
332: - pos, (int) intersection.length());
333: }
334: pos += length;
335: }
336: } else {
337: if (contentLength != -1) {
338: response.setHeader("Content-Length", Long
339: .toString(contentLength));
340: }
341:
342: while ((length = inputStream.read(buffer)) > -1) {
343: out.write(buffer, 0, length);
344: }
345: }
346:
347: out.flush();
348: }
349:
350: /**
351: * Generates the requested resource.
352: */
353: public void generate() throws IOException, ProcessingException {
354: try {
355: InputStream inputStream;
356: try {
357: inputStream = inputSource.getInputStream();
358: } catch (SourceException e) {
359: throw SourceUtil
360: .handle(
361: "Error during resolving of the input stream",
362: e);
363: }
364:
365: // Bugzilla Bug #25069: Close inputStream in finally block.
366: try {
367: processStream(inputStream);
368: } finally {
369: if (inputStream != null) {
370: inputStream.close();
371: }
372: }
373:
374: if (!quickTest) {
375: // if everything is ok, add this to the list of generated documents
376: // (see http://marc.theaimsgroup.com/?l=xml-cocoon-dev&m=102921894301915 )
377: documents.put(request.getRequestURI(), inputSource
378: .getURI());
379: }
380: } catch (IOException e) {
381: getLogger()
382: .debug(
383: "Received an IOException, assuming client severed connection on purpose");
384: }
385: }
386:
387: /**
388: * Returns the mime-type of the resource in process.
389: */
390: public String getMimeType() {
391: Context ctx = ObjectModelHelper.getContext(objectModel);
392: if (ctx != null) {
393: final String mimeType = ctx.getMimeType(source);
394: if (mimeType != null) {
395: return mimeType;
396: }
397: }
398: return inputSource.getMimeType();
399: }
400: }
|