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.source.impl;
018:
019: import java.io.ByteArrayInputStream;
020: import java.io.ByteArrayOutputStream;
021: import java.io.IOException;
022: import java.io.InputStream;
023: import java.io.Serializable;
024: import java.net.MalformedURLException;
025: import java.util.Iterator;
026: import java.util.Map;
027:
028: import org.apache.avalon.framework.component.Component;
029: import org.apache.avalon.framework.component.ComponentManager;
030: import org.apache.avalon.framework.logger.AbstractLogEnabled;
031: import org.apache.avalon.framework.logger.Logger;
032: import org.apache.cocoon.Constants;
033: import org.apache.cocoon.Processor;
034: import org.apache.cocoon.ResourceNotFoundException;
035: import org.apache.cocoon.components.CocoonComponentManager;
036: import org.apache.cocoon.components.pipeline.ProcessingPipeline;
037: import org.apache.cocoon.components.source.SourceUtil;
038: import org.apache.cocoon.environment.Environment;
039: import org.apache.cocoon.environment.ObjectModelHelper;
040: import org.apache.cocoon.environment.wrapper.EnvironmentWrapper;
041: import org.apache.cocoon.environment.wrapper.MutableEnvironmentFacade;
042: import org.apache.cocoon.xml.ContentHandlerWrapper;
043: import org.apache.cocoon.xml.XMLConsumer;
044: import org.apache.excalibur.source.Source;
045: import org.apache.excalibur.source.SourceException;
046: import org.apache.excalibur.source.SourceNotFoundException;
047: import org.apache.excalibur.source.SourceResolver;
048: import org.apache.excalibur.source.SourceValidity;
049: import org.apache.excalibur.xml.sax.XMLizable;
050: import org.xml.sax.ContentHandler;
051: import org.xml.sax.SAXException;
052: import org.xml.sax.ext.LexicalHandler;
053:
054: /**
055: * Implementation of a {@link Source} that gets its content
056: * by invoking a pipeline.
057: *
058: * @author <a href="mailto:cziegeler@apache.org">Carsten Ziegeler</a>
059: * @version $Id: SitemapSource.java 540711 2007-05-22 19:36:07Z cziegeler $
060: */
061: public final class SitemapSource extends AbstractLogEnabled implements
062: Source, XMLizable {
063:
064: /** The internal event pipeline validities */
065: private SitemapSourceValidity validity;
066:
067: /** The system id */
068: private final String systemId;
069:
070: /** The system id used for caching */
071: private String systemIdForCaching;
072:
073: /** The current ComponentManager */
074: private final ComponentManager manager;
075:
076: /** The processor */
077: private final Processor processor;
078:
079: /** The pipeline processor */
080: private Processor pipelineProcessor;
081:
082: /** The environment */
083: private final MutableEnvironmentFacade environment;
084:
085: /** The <code>ProcessingPipeline</code> */
086: private ProcessingPipeline processingPipeline;
087:
088: /** The redirect <code>Source</code> */
089: private Source redirectSource;
090:
091: /** The <code>SAXException</code> if unable to get resource */
092: private SAXException exception;
093:
094: /** Do I need a refresh ? */
095: private boolean needsRefresh;
096:
097: /** The unique key for this processing */
098: private Object processKey;
099:
100: /** The used protocol */
101: private final String protocol;
102:
103: /** SourceResolver (for the redirect source) */
104: private SourceResolver sourceResolver;
105:
106: private String mimeType;
107:
108: /**
109: * Construct a new object
110: */
111: public SitemapSource(ComponentManager manager, String uri,
112: Map parameters, Logger logger) throws MalformedURLException {
113:
114: Environment env = CocoonComponentManager
115: .getCurrentEnvironment();
116: if (env == null) {
117: throw new MalformedURLException(
118: "The cocoon protocol can not be used outside an environment.");
119: }
120:
121: this .manager = manager;
122: this .enableLogging(logger);
123:
124: boolean rawMode = false;
125:
126: // remove the protocol
127: int position = uri.indexOf(':') + 1;
128: if (position != 0) {
129: this .protocol = uri.substring(0, position - 1);
130: // check for subprotocol
131: if (uri.startsWith("raw:", position)) {
132: position += 4;
133: rawMode = true;
134: }
135: } else {
136: throw new MalformedURLException(
137: "No protocol found for sitemap source in " + uri);
138: }
139:
140: // does the uri point to this sitemap or to the root sitemap?
141: String prefix;
142: if (uri.startsWith("//", position)) {
143: position += 2;
144: this .processor = CocoonComponentManager.getActiveProcessor(
145: env).getRootProcessor();
146: prefix = ""; // start at the root
147: } else if (uri.startsWith("/", position)) {
148: position++;
149: prefix = null;
150: this .processor = CocoonComponentManager
151: .getActiveProcessor(env);
152: } else {
153: throw new MalformedURLException("Malformed cocoon URI: "
154: + uri);
155: }
156:
157: // create the queryString (if available)
158: String queryString = null;
159: int queryStringPos = uri.indexOf('?', position);
160: if (queryStringPos != -1) {
161: queryString = uri.substring(queryStringPos + 1);
162: uri = uri.substring(position, queryStringPos);
163: } else if (position > 0) {
164: uri = uri.substring(position);
165: }
166:
167: // determine if the queryString specifies a cocoon-view
168: String view = null;
169: if (queryString != null) {
170: int index = queryString.indexOf(Constants.VIEW_PARAM);
171: if (index != -1
172: && (index == 0 || queryString.charAt(index - 1) == '&')
173: && queryString.length() > index
174: + Constants.VIEW_PARAM.length()
175: && queryString.charAt(index
176: + Constants.VIEW_PARAM.length()) == '=') {
177:
178: String tmp = queryString.substring(index
179: + Constants.VIEW_PARAM.length() + 1);
180: index = tmp.indexOf('&');
181: if (index != -1) {
182: view = tmp.substring(0, index);
183: } else {
184: view = tmp;
185: }
186: } else {
187: view = env.getView();
188: }
189: } else {
190: view = env.getView();
191: }
192:
193: // build the request uri which is relative to the context
194: String requestURI = (prefix == null ? env.getURIPrefix() + uri
195: : uri);
196:
197: // create system ID
198: this .systemId = queryString == null ? this .protocol + "://"
199: + requestURI : this .protocol + "://" + requestURI + "?"
200: + queryString;
201:
202: // create environment...
203: EnvironmentWrapper wrapper = new EnvironmentWrapper(env,
204: requestURI, queryString, logger, manager, rawMode, view);
205: wrapper.setURI(prefix, uri);
206:
207: // The environment is a facade whose delegate can be changed in case of internal redirects
208: this .environment = new MutableEnvironmentFacade(wrapper);
209:
210: // ...and put information passed from the parent request to the internal request
211: if (null != parameters) {
212: this .environment.getObjectModel().put(
213: ObjectModelHelper.PARENT_CONTEXT, parameters);
214: } else {
215: this .environment.getObjectModel().remove(
216: ObjectModelHelper.PARENT_CONTEXT);
217: }
218:
219: // create a new validity holder
220: this .validity = new SitemapSourceValidity();
221:
222: // initialize
223: this .init();
224: }
225:
226: /**
227: * Return the protocol identifier.
228: */
229: public String getScheme() {
230: return this .protocol;
231: }
232:
233: /**
234: * Get the content length of the source or -1 if it
235: * is not possible to determine the length.
236: */
237: public long getContentLength() {
238: return -1;
239: }
240:
241: /**
242: * Get the last modification date.
243: * @return The last modification in milliseconds since January 1, 1970 GMT
244: * or 0 if it is unknown
245: */
246: public long getLastModified() {
247: return 0;
248: }
249:
250: /**
251: * Return an <code>InputStream</code> object to read from the source.
252: */
253: public InputStream getInputStream() throws IOException,
254: SourceException {
255:
256: if (this .needsRefresh) {
257: this .refresh();
258: }
259: // VG: Why exception is not thrown in constructor?
260: if (this .exception != null) {
261: throw new SourceException("Cannot get input stream for "
262: + this .getURI(), this .exception);
263: }
264:
265: if (this .redirectSource != null) {
266: return this .redirectSource.getInputStream();
267: }
268:
269: try {
270: ByteArrayOutputStream os = new ByteArrayOutputStream();
271: this .environment.setOutputStream(os);
272: CocoonComponentManager.enterEnvironment(this .environment,
273: this .manager, this .pipelineProcessor);
274: try {
275: this .processingPipeline.process(this .environment);
276: } finally {
277: CocoonComponentManager.leaveEnvironment();
278: }
279:
280: return new ByteArrayInputStream(os.toByteArray());
281:
282: } catch (ResourceNotFoundException e) {
283: throw new SourceNotFoundException(
284: "Exception during processing of " + this .systemId,
285: e);
286: } catch (Exception e) {
287: throw new SourceException("Exception during processing of "
288: + this .systemId, e);
289: } finally {
290: // Unhide wrapped environment output stream
291: this .environment.setOutputStream(null);
292: this .needsRefresh = true;
293: }
294: }
295:
296: /**
297: * Returns the unique identifer for this source
298: */
299: public String getURI() {
300: return this .systemIdForCaching;
301: }
302:
303: /**
304: * Returns true always.
305: * @see org.apache.excalibur.source.Source#exists()
306: */
307: public boolean exists() {
308: return true;
309: }
310:
311: /**
312: * Get the validity object. This wraps validity of the enclosed event
313: * pipeline. If pipeline is not cacheable, <code>null</code> is returned.
314: */
315: public SourceValidity getValidity() {
316: return this .validity.getNestedValidity() == null ? null
317: : this .validity;
318: }
319:
320: /**
321: * The mime-type of the content described by this object.
322: * If the source is not able to determine the mime-type by itself
323: * this can be null.
324: */
325: public String getMimeType() {
326: return this .mimeType;
327: }
328:
329: /**
330: * Refresh this object and update the last modified date
331: * and content length.
332: */
333: public void refresh() {
334: this .reset();
335: this .init();
336: }
337:
338: /**
339: * Initialize
340: */
341: protected void init() {
342: this .systemIdForCaching = this .systemId;
343: try {
344: this .processKey = CocoonComponentManager
345: .startProcessing(this .environment);
346: this .processingPipeline = this .processor
347: .buildPipeline(this .environment);
348: this .pipelineProcessor = CocoonComponentManager
349: .getActiveProcessor(this .environment);
350:
351: String redirectURL = this .environment.getRedirectURL();
352: if (redirectURL == null) {
353:
354: CocoonComponentManager.enterEnvironment(
355: this .environment, this .manager,
356: this .pipelineProcessor);
357: try {
358: this .processingPipeline
359: .prepareInternal(this .environment);
360: this .validity.set(this .processingPipeline
361: .getValidityForEventPipeline());
362: this .mimeType = this .environment.getContentType();
363:
364: final String eventPipelineKey = this .processingPipeline
365: .getKeyForEventPipeline();
366: if (eventPipelineKey != null) {
367: StringBuffer buffer = new StringBuffer(
368: this .systemId);
369: if (this .systemId.indexOf('?') == -1) {
370: buffer.append('?');
371: } else {
372: buffer.append('&');
373: }
374: buffer.append("pipelinehash=");
375: buffer.append(eventPipelineKey);
376: this .systemIdForCaching = buffer.toString();
377: } else {
378: this .systemIdForCaching = this .systemId;
379: }
380: } finally {
381: CocoonComponentManager.leaveEnvironment();
382: }
383: } else {
384: if (redirectURL.indexOf(":") == -1) {
385: redirectURL = this .protocol + ":/" + redirectURL;
386: }
387: if (this .sourceResolver == null) {
388: this .sourceResolver = (SourceResolver) this .manager
389: .lookup(SourceResolver.ROLE);
390: }
391: this .redirectSource = this .sourceResolver
392: .resolveURI(redirectURL);
393: this .validity.set(this .redirectSource.getValidity());
394: this .mimeType = this .redirectSource.getMimeType();
395: }
396: } catch (SAXException e) {
397: this .reset();
398: this .exception = e;
399: } catch (Exception e) {
400: this .reset();
401: this .exception = new SAXException(
402: "Could not get sitemap source " + this .systemId, e);
403: }
404: this .needsRefresh = false;
405: }
406:
407: /**
408: * Stream content to the content handler
409: */
410: public void toSAX(ContentHandler contentHandler)
411: throws SAXException {
412: if (this .needsRefresh) {
413: this .refresh();
414: }
415: if (this .exception != null) {
416: throw this .exception;
417: }
418: try {
419: if (this .redirectSource != null) {
420: SourceUtil.parse(this .manager, this .redirectSource,
421: contentHandler);
422: } else {
423: XMLConsumer consumer;
424: if (contentHandler instanceof XMLConsumer) {
425: consumer = (XMLConsumer) contentHandler;
426: } else if (contentHandler instanceof LexicalHandler) {
427: consumer = new ContentHandlerWrapper(
428: contentHandler,
429: (LexicalHandler) contentHandler);
430: } else {
431: consumer = new ContentHandlerWrapper(contentHandler);
432: }
433: // We have to add an environment changer
434: // for clean environment stack handling.
435: CocoonComponentManager.enterEnvironment(
436: this .environment, this .manager,
437: this .pipelineProcessor);
438: try {
439: this .processingPipeline
440: .process(
441: this .environment,
442: CocoonComponentManager
443: .createEnvironmentAwareConsumer(consumer));
444: } finally {
445: CocoonComponentManager.leaveEnvironment();
446: }
447: }
448: } catch (SAXException e) {
449: // Preserve original exception
450: throw e;
451: } catch (Exception e) {
452: throw new SAXException("Exception during processing of "
453: + this .systemId, e);
454: } finally {
455: this .needsRefresh = true;
456: }
457: }
458:
459: /**
460: * Reset everything
461: */
462: private void reset() {
463: if (this .processingPipeline != null) {
464: this .processingPipeline.release();
465: this .processingPipeline = null;
466: }
467:
468: if (this .processKey != null) {
469: CocoonComponentManager.endProcessing(this .environment,
470: this .processKey);
471: this .processKey = null;
472: }
473:
474: if (this .redirectSource != null) {
475: this .sourceResolver.release(this .redirectSource);
476: this .redirectSource = null;
477: }
478:
479: this .validity.set(null);
480:
481: this .environment.reset();
482: this .exception = null;
483: this .needsRefresh = true;
484: this .pipelineProcessor = null;
485: }
486:
487: /**
488: * Recyclable
489: */
490: public void recycle() {
491: this .validity = new SitemapSourceValidity();
492: this .reset();
493: if (this .sourceResolver != null) {
494: this .manager.release((Component) this .sourceResolver);
495: this .sourceResolver = null;
496: }
497: }
498:
499: /**
500: * Get the value of a parameter.
501: * Using this it is possible to get custom information provided by the
502: * source implementation, like an expires date, HTTP headers etc.
503: */
504: public String getParameter(String name) {
505: return null;
506: }
507:
508: /**
509: * Get the value of a parameter.
510: * Using this it is possible to get custom information provided by the
511: * source implementation, like an expires date, HTTP headers etc.
512: */
513: public long getParameterAsLong(String name) {
514: return 0;
515: }
516:
517: /**
518: * Get parameter names
519: * Using this it is possible to get custom information provided by the
520: * source implementation, like an expires date, HTTP headers etc.
521: */
522: public Iterator getParameterNames() {
523: return java.util.Collections.EMPTY_LIST.iterator();
524: }
525:
526: /**
527: * A simple SourceValidity protecting callers from resets.
528: */
529: public static final class SitemapSourceValidity implements
530: SourceValidity, Serializable {
531:
532: private SourceValidity validity;
533:
534: private SitemapSourceValidity() {
535: super ();
536: }
537:
538: void set(SourceValidity validity) {
539: this .validity = validity;
540: }
541:
542: public int isValid() {
543: return (this .validity != null ? this .validity.isValid()
544: : SourceValidity.INVALID);
545: }
546:
547: public int isValid(SourceValidity validity) {
548: if (validity instanceof SitemapSourceValidity) {
549: return (this .validity != null ? this .validity
550: .isValid(((SitemapSourceValidity) validity)
551: .getNestedValidity())
552: : SourceValidity.INVALID);
553: }
554: return SourceValidity.INVALID;
555: }
556:
557: public SourceValidity getNestedValidity() {
558: return this.validity;
559: }
560: }
561: }
|