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.components.pipeline.impl;
018:
019: import org.apache.avalon.framework.component.ComponentException;
020: import org.apache.avalon.framework.parameters.ParameterException;
021: import org.apache.avalon.framework.parameters.Parameters;
022:
023: import org.apache.cocoon.ProcessingException;
024: import org.apache.cocoon.caching.CachedResponse;
025: import org.apache.cocoon.caching.CachingOutputStream;
026: import org.apache.cocoon.caching.IdentifierCacheKey;
027: import org.apache.cocoon.components.sax.XMLDeserializer;
028: import org.apache.cocoon.components.sax.XMLSerializer;
029: import org.apache.cocoon.components.sax.XMLTeePipe;
030: import org.apache.cocoon.environment.Environment;
031: import org.apache.cocoon.environment.ObjectModelHelper;
032: import org.apache.cocoon.environment.Response;
033: import org.apache.cocoon.xml.XMLConsumer;
034:
035: import org.apache.excalibur.source.SourceValidity;
036: import org.apache.excalibur.source.impl.validity.ExpiresValidity;
037: import org.apache.excalibur.source.impl.validity.NOPValidity;
038:
039: import java.io.ByteArrayOutputStream;
040: import java.io.OutputStream;
041: import java.util.Map;
042:
043: /**
044: * This pipeline implementation caches the complete content for a defined
045: * period of time (expires).
046: *
047: * <map:pipe name="expires" src="org.apache.cocoon.components.pipeline.impl.ExpiresCachingProcessingPipeline">
048: * <parameter name="cache-expires" value="180"/> <!-- Expires in secondes -->
049: * </map:pipe>
050: *
051: * The cache-expires parameter controls the period of time for caching the content. A positive
052: * value is a value in seconds, a value of zero means no caching and a negative value means
053: * indefinite caching. In this case, you should use an external mechanism to invalidate the
054: * cache entry.
055: *
056: * @since 2.1
057: * @author <a href="mailto:cziegeler@apache.org">Carsten Ziegeler</a>
058: * @version CVS $Id: ExpiresCachingProcessingPipeline.java 502603 2007-02-02 13:29:01Z cziegeler $
059: */
060: public class ExpiresCachingProcessingPipeline extends
061: BaseCachingProcessingPipeline {
062:
063: /** This key can be used to put a key in the object model */
064: public static final String CACHE_KEY_KEY = ExpiresCachingProcessingPipeline.class
065: .getName()
066: + "/CacheKey";
067:
068: /** This key can be used to put an expires information in the object model */
069: public static final String CACHE_EXPIRES_KEY = ExpiresCachingProcessingPipeline.class
070: .getName()
071: + "/Expires";
072:
073: /** The source validity */
074: protected SourceValidity cacheValidity;
075:
076: /** The key used for caching */
077: protected IdentifierCacheKey cacheKey;
078:
079: /** The expires information. */
080: protected long cacheExpires;
081:
082: /** Default value for expiration */
083: protected long defaultCacheExpires = 3600; // 1 hour
084:
085: /** The cached response */
086: protected CachedResponse cachedResponse;
087:
088: public void parameterize(Parameters params)
089: throws ParameterException {
090: super .parameterize(params);
091:
092: this .defaultCacheExpires = params.getParameterAsLong(
093: "cache-expires", this .defaultCacheExpires);
094: }
095:
096: /**
097: * Process the given <code>Environment</code>, producing the output.
098: */
099: protected boolean processXMLPipeline(Environment environment)
100: throws ProcessingException {
101: try {
102: if (this .cachedResponse != null) {
103: byte[] content = cachedResponse.getResponse();
104:
105: if (this .serializer == this .lastConsumer) {
106: if (cachedResponse.getContentType() != null) {
107: environment.setContentType(cachedResponse
108: .getContentType());
109: } else {
110: this .setMimeTypeForSerializer(environment);
111: }
112: final OutputStream outputStream = environment
113: .getOutputStream(0);
114: if (content.length > 0) {
115: environment.setContentLength(content.length);
116: outputStream.write(content);
117: }
118: } else {
119: this .setMimeTypeForSerializer(environment);
120: this .xmlDeserializer.setConsumer(this .lastConsumer);
121: this .xmlDeserializer.deserialize(content);
122: }
123:
124: } else {
125:
126: // generate new response
127:
128: if (this .cacheExpires == 0) {
129: return super .processXMLPipeline(environment);
130: }
131:
132: this .setMimeTypeForSerializer(environment);
133: byte[] cachedData;
134: if (this .serializer == this .lastConsumer) {
135:
136: if (this .serializer.shouldSetContentLength()) {
137: OutputStream os = environment
138: .getOutputStream(this .outputBufferSize);
139:
140: // set the output stream
141: ByteArrayOutputStream baos = new ByteArrayOutputStream();
142: this .serializer.setOutputStream(baos);
143:
144: this .generator.generate();
145:
146: cachedData = baos.toByteArray();
147: environment.setContentLength(cachedData.length);
148: os.write(cachedData);
149: } else {
150: CachingOutputStream os = new CachingOutputStream(
151: environment
152: .getOutputStream(this .outputBufferSize));
153: // set the output stream
154: this .serializer.setOutputStream(os);
155: this .generator.generate();
156:
157: cachedData = os.getContent();
158: }
159:
160: } else {
161: this .generator.generate();
162: cachedData = (byte[]) this .xmlSerializer
163: .getSAXFragment();
164: }
165:
166: //
167: // Now that we have processed the pipeline,
168: // we do the actual caching
169: //
170: if (this .cacheValidity != null) {
171: cachedResponse = new CachedResponse(
172: this .cacheValidity, cachedData);
173: cachedResponse.setContentType(environment
174: .getContentType());
175: this .cache.store(this .cacheKey, cachedResponse);
176: }
177: }
178: } catch (Exception e) {
179: handleException(e);
180: }
181:
182: return true;
183: }
184:
185: /**
186: * Connect the XML pipeline.
187: */
188: protected void connectPipeline(Environment environment)
189: throws ProcessingException {
190: if (this .lastConsumer != this .serializer) {
191: // internal
192: if (this .cachedResponse == null) {
193: // if we cache, we need an xml serializer
194: if (this .cacheExpires != 0) {
195: try {
196: final XMLConsumer old = this .lastConsumer;
197: this .xmlSerializer = (XMLSerializer) this .manager
198: .lookup(XMLSerializer.ROLE);
199: this .lastConsumer = new XMLTeePipe(
200: this .lastConsumer, this .xmlSerializer);
201:
202: super .connectPipeline(environment);
203:
204: this .lastConsumer = old;
205: } catch (ComponentException e) {
206: throw new ProcessingException(
207: "Could not connect pipeline.", e);
208: }
209: } else {
210: super .connectPipeline(environment);
211: }
212: } else {
213: // we use the cache, so we need an xml deserializer
214: try {
215: this .xmlDeserializer = (XMLDeserializer) this .manager
216: .lookup(XMLDeserializer.ROLE);
217: } catch (ComponentException e) {
218: throw new ProcessingException(
219: "Could not connect pipeline.", e);
220: }
221: }
222: } else {
223: // external: we only need to connect if we don't use a cached response
224: if (this .cachedResponse == null) {
225: super .connectPipeline(environment);
226: }
227: }
228: }
229:
230: /**
231: * Prepare the pipeline
232: */
233: protected void preparePipeline(Environment environment)
234: throws ProcessingException {
235: // prepare pipeline only if we don't have a cached response yet
236: if (this .cachedResponse == null)
237: super .preparePipeline(environment);
238: }
239:
240: protected void setupPipeline(Environment environment)
241: throws ProcessingException {
242: // get the key and the expires info
243: // we must do this before we call super.preparePipeline,
244: // otherwise internal pipelines are instantiated and
245: // get a copy of the object model with our info!
246: final Map objectModel = environment.getObjectModel();
247: String key = (String) objectModel.get(CACHE_KEY_KEY);
248: if (key == null) {
249: key = this .parameters.getParameter("cache-key", null);
250: if (key == null) {
251: key = environment.getURIPrefix() + environment.getURI();
252: }
253: } else {
254: objectModel.remove(CACHE_KEY_KEY);
255: }
256: String expiresValue = (String) objectModel
257: .get(CACHE_EXPIRES_KEY);
258: if (expiresValue == null) {
259: this .cacheExpires = this .parameters.getParameterAsLong(
260: "cache-expires", this .defaultCacheExpires);
261: } else {
262: this .cacheExpires = Long.valueOf(expiresValue).longValue();
263: objectModel.remove(CACHE_EXPIRES_KEY);
264: }
265:
266: // and now prepare the caching information
267: this .cacheKey = new IdentifierCacheKey(key,
268: this .serializer == this .lastConsumer);
269: if (this .cacheExpires > 0) {
270: this .cacheValidity = new ExpiresValidity(
271: this .cacheExpires * 1000);
272: } else if (this .cacheExpires < 0) {
273: this .cacheValidity = NOPValidity.SHARED_INSTANCE;
274: }
275: final boolean purge = this .parameters.getParameterAsBoolean(
276: "purge-cache", false);
277:
278: this .cachedResponse = this .cache.get(this .cacheKey);
279: if (this .cachedResponse != null) {
280: final SourceValidity sv = cachedResponse
281: .getValidityObjects()[0];
282: if (purge
283: || (this .cacheExpires != -1 && sv.isValid() != SourceValidity.VALID)) {
284: this .cache.remove(this .cacheKey);
285: this .cachedResponse = null;
286: }
287: }
288: if (this .cacheExpires > 0
289: && (this .reader != null || this .lastConsumer == this .serializer)) {
290: Response res = ObjectModelHelper.getResponse(environment
291: .getObjectModel());
292: res.setDateHeader("Expires", System.currentTimeMillis()
293: + (this .cacheExpires * 1000));
294: res.setHeader("Cache-Control", "max-age="
295: + this .cacheExpires + ", public");
296: }
297:
298: // only actually set up pipeline components when there was no cached response found for the specified key
299: if (this .cachedResponse == null) {
300: super .setupPipeline(environment);
301: }
302: }
303:
304: /**
305: * Return valid validity objects for the event pipeline
306: * If the "event pipeline" (= the complete pipeline without the
307: * serializer) is cacheable and valid, return all validity objects.
308: * Otherwise return <code>null</code>
309: */
310: public SourceValidity getValidityForEventPipeline() {
311: return this .cacheValidity;
312: }
313:
314: /* (non-Javadoc)
315: * @see org.apache.cocoon.components.pipeline.ProcessingPipeline#getKeyForEventPipeline()
316: */
317: public String getKeyForEventPipeline() {
318: if (this .cacheKey != null && this .cacheValidity != null) {
319: return this .cacheKey.toString();
320: }
321: return null;
322: }
323:
324: /**
325: * Recyclable Interface
326: */
327: public void recycle() {
328: this .cacheKey = null;
329: this .cacheExpires = 0;
330: this .cachedResponse = null;
331: super .recycle();
332: }
333:
334: /* (non-Javadoc)
335: * @see org.apache.cocoon.components.pipeline.AbstractProcessingPipeline#processReader(org.apache.cocoon.environment.Environment)
336: */
337: protected boolean processReader(Environment environment)
338: throws ProcessingException {
339: try {
340: if (this .cachedResponse != null) {
341: if (cachedResponse.getContentType() != null) {
342: environment.setContentType(cachedResponse
343: .getContentType());
344: } else {
345: this .setMimeTypeForReader(environment);
346: }
347:
348: final byte[] content = cachedResponse.getResponse();
349: environment.setContentLength(content.length);
350:
351: final OutputStream os = environment.getOutputStream(0);
352: os.write(content);
353:
354: } else {
355: // generate new response
356:
357: if (this .cacheExpires == 0) {
358: return super .processReader(environment);
359: }
360:
361: byte[] cachedData;
362:
363: this .setMimeTypeForReader(environment);
364: if (this .reader.shouldSetContentLength()) {
365: final OutputStream os = environment
366: .getOutputStream(this .outputBufferSize);
367:
368: // set the output stream
369: final ByteArrayOutputStream baos = new ByteArrayOutputStream();
370: this .reader.setOutputStream(baos);
371:
372: this .reader.generate();
373:
374: cachedData = baos.toByteArray();
375: environment.setContentLength(cachedData.length);
376: os.write(cachedData);
377: } else {
378: final CachingOutputStream os = new CachingOutputStream(
379: environment
380: .getOutputStream(this .outputBufferSize));
381: // set the output stream
382: this .reader.setOutputStream(os);
383: this .reader.generate();
384:
385: cachedData = os.getContent();
386: }
387:
388: //
389: // Now that we have processed the pipeline,
390: // we do the actual caching
391: //
392: if (this .cacheValidity != null) {
393: cachedResponse = new CachedResponse(
394: this .cacheValidity, cachedData);
395: cachedResponse.setContentType(environment
396: .getContentType());
397: this .cache.store(this .cacheKey, cachedResponse);
398: }
399: }
400: } catch (Exception e) {
401: handleException(e);
402: }
403:
404: return true;
405: }
406: }
|