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