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.serialization;
018:
019: import java.io.FilterOutputStream;
020: import java.io.IOException;
021: import java.io.InputStream;
022: import java.util.Enumeration;
023: import java.util.zip.ZipEntry;
024: import java.util.zip.ZipOutputStream;
025:
026: import org.apache.avalon.framework.activity.Disposable;
027: import org.apache.avalon.framework.service.ServiceException;
028: import org.apache.avalon.framework.service.ServiceManager;
029: import org.apache.avalon.framework.service.ServiceSelector;
030: import org.apache.avalon.framework.service.Serviceable;
031: import org.apache.excalibur.source.Source;
032: import org.apache.excalibur.source.SourceResolver;
033: import org.xml.sax.Attributes;
034: import org.xml.sax.SAXException;
035: import org.xml.sax.helpers.NamespaceSupport;
036:
037: /**
038: * A serializer that builds Zip archives by aggregating several sources.
039: * <p>
040: * The input document should describe entries of the archive by means of
041: * their name (which can be a path) and their content either as URLs or
042: * inline data :
043: * <ul>
044: * <li>URLs, given by the "src" attribute, are Cocoon sources and as such
045: * can use any of the protocols handled by Cocoon, including "cocoon:" to
046: * include dynamically generated content in the archive.</li>
047: * <li>inline data is represented by an XML document that is serialized to the
048: * zip entry using the serializer identified by the "serializer" attribute.</li>
049: * </ul>
050: * <p>
051: * Example :
052: * <pre>
053: * <zip:archive xmlns:zip="http://apache.org/cocoon/zip-archive/1.0">
054: * <zip:entry name="foo.html" src="cocoon://dynFoo.html"/>
055: * <zip:entry name="images/bar.jpeg" src="bar.jpeg"/>
056: * <zip:entry name="index.html" serializer="html">
057: * <html>
058: * <head>
059: * <title>Index page</title>
060: * </head>
061: * <body>
062: * Please go <a href="foo.html">there</a>
063: * </body<
064: * </html>
065: * </zip:entry>
066: * </zip:archive:zip>
067: * </pre>
068: *
069: * @author <a href="http://www.apache.org/~sylvain">Sylvain Wallez</a>
070: * @version $Id: ZipArchiveSerializer.java 437692 2006-08-28 13:09:39Z anathaniel $
071: */
072:
073: // TODO (1) : handle more attributes on <archive> for properties of ZipOutputStream
074: // such as comment or default compression method and level
075: // TODO (2) : handle more attributes on <entry> for properties of ZipEntry
076: // (compression method and level, time, comment, etc.)
077: public class ZipArchiveSerializer extends AbstractSerializer implements
078: Disposable, Serviceable {
079:
080: /**
081: * The namespace for elements handled by this serializer,
082: * "http://apache.org/cocoon/zip-archive/1.0".
083: */
084: public static final String ZIP_NAMESPACE = "http://apache.org/cocoon/zip-archive/1.0";
085:
086: private static final int START_STATE = 0;
087: private static final int IN_ZIP_STATE = 1;
088: private static final int IN_CONTENT_STATE = 2;
089:
090: /** The component manager */
091: protected ServiceManager manager;
092:
093: /** The serializer component selector */
094: protected ServiceSelector selector;
095:
096: /** The Zip stream where entries will be written */
097: protected ZipOutputStream zipOutput;
098:
099: /** The current state */
100: protected int state = START_STATE;
101:
102: /** The resolver to get sources */
103: protected SourceResolver resolver;
104:
105: /** Temporary byte buffer to read source data */
106: protected byte[] buffer;
107:
108: /** Serializer used when in IN_CONTENT state */
109: protected Serializer serializer;
110:
111: /** Current depth of the serialized content */
112: protected int contentDepth;
113:
114: /** Used to collect namespaces */
115: private NamespaceSupport nsSupport = new NamespaceSupport();
116:
117: /**
118: * Store exception
119: */
120: private SAXException exception;
121:
122: /**
123: * @see org.apache.avalon.framework.service.Serviceable#service(ServiceManager)
124: */
125: public void service(ServiceManager manager) throws ServiceException {
126: this .manager = manager;
127: this .resolver = (SourceResolver) this .manager
128: .lookup(SourceResolver.ROLE);
129: }
130:
131: /**
132: * Returns default mime type for zip archives, <code>application/zip</code>.
133: * Can be overridden in the sitemap.
134: * @return application/zip
135: */
136: public String getMimeType() {
137: return "application/zip";
138: }
139:
140: /**
141: * @see org.xml.sax.ContentHandler#startDocument()
142: */
143: public void startDocument() throws SAXException {
144: this .state = START_STATE;
145: this .zipOutput = new ZipOutputStream(this .output);
146: }
147:
148: /**
149: * Begin the scope of a prefix-URI Namespace mapping.
150: *
151: * @param prefix The Namespace prefix being declared.
152: * @param uri The Namespace URI the prefix is mapped to.
153: */
154: public void startPrefixMapping(String prefix, String uri)
155: throws SAXException {
156: if (state == IN_CONTENT_STATE && this .contentDepth > 0) {
157: // Pass to the serializer
158: super .startPrefixMapping(prefix, uri);
159:
160: } else {
161: // Register it if it's not our own namespace (useless to content)
162: if (!uri.equals(ZIP_NAMESPACE)) {
163: this .nsSupport.declarePrefix(prefix, uri);
164: }
165: }
166: }
167:
168: public void endPrefixMapping(String prefix) throws SAXException {
169: if (state == IN_CONTENT_STATE && this .contentDepth > 0) {
170: // Pass to the serializer
171: super .endPrefixMapping(prefix);
172: }
173: }
174:
175: // Note : no need to implement endPrefixMapping() as we just need to pass it through if there
176: // is a serializer, which is what the superclass does.
177:
178: /**
179: * @see org.xml.sax.ContentHandler#startElement(String, String, String, Attributes)
180: */
181: public void startElement(String namespaceURI, String localName,
182: String qName, Attributes atts) throws SAXException {
183:
184: // Damage control. Sometimes one exception is just not enough...
185: if (this .exception != null) {
186: throw this .exception;
187: }
188:
189: switch (state) {
190: case START_STATE:
191: // expecting "zip" as the first element
192: if (namespaceURI.equals(ZIP_NAMESPACE)
193: && localName.equals("archive")) {
194: this .nsSupport.pushContext();
195: this .state = IN_ZIP_STATE;
196: } else {
197: throw this .exception = new SAXException(
198: "Expecting 'archive' root element (got '"
199: + localName + "')");
200: }
201: break;
202:
203: case IN_ZIP_STATE:
204: // expecting "entry" element
205: if (namespaceURI.equals(ZIP_NAMESPACE)
206: && localName.equals("entry")) {
207: this .nsSupport.pushContext();
208: // Get the source
209: addEntry(atts);
210: } else {
211: throw this .exception = new SAXException(
212: "Expecting 'entry' element (got '" + localName
213: + "')");
214: }
215: break;
216:
217: case IN_CONTENT_STATE:
218: if (this .contentDepth == 0) {
219: // Give it any namespaces already declared
220: Enumeration prefixes = this .nsSupport.getPrefixes();
221: while (prefixes.hasMoreElements()) {
222: String prefix = (String) prefixes.nextElement();
223: super .startPrefixMapping(prefix, this .nsSupport
224: .getURI(prefix));
225: }
226: }
227:
228: this .contentDepth++;
229: super .startElement(namespaceURI, localName, qName, atts);
230: break;
231: }
232: }
233:
234: /**
235: * @see org.xml.sax.ContentHandler#characters(char[], int, int)
236: */
237: public void characters(char[] buffer, int offset, int length)
238: throws SAXException {
239: // Propagate text to the serializer only if we have encountered the content's top-level
240: // element. Otherwhise, the serializer may be confused by some characters occuring between
241: // startDocument() and the first startElement() (e.g. Batik fails hard in that case)
242: if (this .state == IN_CONTENT_STATE && this .contentDepth > 0) {
243: super .characters(buffer, offset, length);
244: }
245: }
246:
247: /**
248: * Add an entry in the archive.
249: * @param atts the attributes that describe the entry
250: */
251: protected void addEntry(Attributes atts) throws SAXException {
252: String name = atts.getValue("name");
253: if (name == null) {
254: throw this .exception = new SAXException(
255: "No name given to the Zip entry");
256: }
257:
258: String src = atts.getValue("src");
259: String serializerType = atts.getValue("serializer");
260:
261: if (src == null && serializerType == null) {
262: throw this .exception = new SAXException(
263: "No source nor serializer given for the Zip entry '"
264: + name + "'");
265: }
266:
267: if (src != null && serializerType != null) {
268: throw this .exception = new SAXException(
269: "Cannot specify both 'src' and 'serializer' on a Zip entry '"
270: + name + "'");
271: }
272:
273: Source source = null;
274: try {
275: if (src != null) {
276: // Get the source and its data
277: source = resolver.resolveURI(src);
278: InputStream sourceInput = source.getInputStream();
279:
280: // Create a new Zip entry with file modification time.
281: ZipEntry entry = new ZipEntry(name);
282: long lastModified = source.getLastModified();
283: if (lastModified != 0)
284: entry.setTime(lastModified);
285: this .zipOutput.putNextEntry(entry);
286:
287: // Buffer lazily allocated
288: if (this .buffer == null)
289: this .buffer = new byte[8192];
290:
291: // Copy the source to the zip
292: int len;
293: while ((len = sourceInput.read(this .buffer)) > 0) {
294: this .zipOutput.write(this .buffer, 0, len);
295: }
296:
297: // and close the entry
298: this .zipOutput.closeEntry();
299: // close input stream (to avoid "too many open files" problem)
300: sourceInput.close();
301: } else {
302: // Create a new Zip entry with current time.
303: ZipEntry entry = new ZipEntry(name);
304: this .zipOutput.putNextEntry(entry);
305:
306: // Serialize content
307: if (this .selector == null) {
308: this .selector = (ServiceSelector) this .manager
309: .lookup(Serializer.ROLE + "Selector");
310: }
311:
312: // Get the serializer
313: this .serializer = (Serializer) this .selector
314: .select(serializerType);
315:
316: // Direct its output to the zip file, filtering calls to close()
317: // (we don't want the archive to be closed by the serializer)
318: this .serializer.setOutputStream(new FilterOutputStream(
319: this .zipOutput) {
320: public void close() { /* nothing */
321: }
322: });
323:
324: // Set it as the current XMLConsumer
325: setConsumer(serializer);
326:
327: // start its document
328: this .serializer.startDocument();
329:
330: this .state = IN_CONTENT_STATE;
331: this .contentDepth = 0;
332: }
333:
334: } catch (RuntimeException re) {
335: throw re;
336: } catch (SAXException se) {
337: throw this .exception = se;
338: } catch (Exception e) {
339: throw this .exception = new SAXException(e);
340: } finally {
341: this .resolver.release(source);
342: }
343: }
344:
345: /**
346: * @see org.xml.sax.ContentHandler#endElement(String, String, String)
347: */
348: public void endElement(String namespaceURI, String localName,
349: String qName) throws SAXException {
350:
351: // Damage control. Sometimes one exception is just not enough...
352: if (this .exception != null) {
353: throw this .exception;
354: }
355:
356: if (state == IN_CONTENT_STATE) {
357: super .endElement(namespaceURI, localName, qName);
358: this .contentDepth--;
359:
360: if (this .contentDepth == 0) {
361: // End of this entry
362:
363: // close all declared namespaces.
364: Enumeration prefixes = this .nsSupport.getPrefixes();
365: while (prefixes.hasMoreElements()) {
366: String prefix = (String) prefixes.nextElement();
367: super .endPrefixMapping(prefix);
368: }
369:
370: super .endDocument();
371:
372: try {
373: this .zipOutput.closeEntry();
374: } catch (IOException ioe) {
375: throw this .exception = new SAXException(ioe);
376: }
377:
378: super .setConsumer(null);
379: this .selector.release(this .serializer);
380: this .serializer = null;
381:
382: // Go back to listening for entries
383: this .state = IN_ZIP_STATE;
384: }
385: } else {
386: this .nsSupport.popContext();
387: }
388: }
389:
390: /**
391: * @see org.xml.sax.ContentHandler#endDocument()
392: */
393: public void endDocument() throws SAXException {
394: try {
395: // Close the zip archive
396: this .zipOutput.finish();
397:
398: } catch (IOException ioe) {
399: throw new SAXException(ioe);
400: }
401: }
402:
403: /**
404: * @see org.apache.avalon.excalibur.pool.Recyclable#recycle()
405: */
406: public void recycle() {
407: this .exception = null;
408: if (this .serializer != null) {
409: this .selector.release(this .serializer);
410: }
411: if (this .selector != null) {
412: this .manager.release(this .selector);
413: }
414:
415: this .nsSupport.reset();
416: super .recycle();
417: }
418:
419: /* (non-Javadoc)
420: * @see org.apache.avalon.framework.activity.Disposable#dispose()
421: */
422: public void dispose() {
423: if (this.manager != null) {
424: this.manager.release(this.resolver);
425: this.resolver = null;
426: this.manager = null;
427: }
428: }
429: }
|