001/*
002 *  Copyright 2012 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.core.cocoon;
017
018import java.io.FilterOutputStream;
019import java.io.IOException;
020import java.io.InputStream;
021import java.util.Enumeration;
022
023import org.apache.avalon.framework.activity.Disposable;
024import org.apache.avalon.framework.service.ServiceException;
025import org.apache.avalon.framework.service.ServiceManager;
026import org.apache.avalon.framework.service.ServiceSelector;
027import org.apache.avalon.framework.service.Serviceable;
028import org.apache.cocoon.serialization.AbstractSerializer;
029import org.apache.cocoon.serialization.Serializer;
030import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
031import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
032import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream.UnicodeExtraFieldPolicy;
033import org.apache.excalibur.source.Source;
034import org.apache.excalibur.source.SourceResolver;
035import org.xml.sax.Attributes;
036import org.xml.sax.SAXException;
037import org.xml.sax.helpers.NamespaceSupport;
038
039/**
040 * ZIP archive serializer that makes use of apache commons compress ZipArchiveOutputStream instead of JavaSE's ZipOutputStream.
041 * It's based on cocoon's ZipArchiveSerializer.
042 */
043public class ZipArchiveNGSerializer extends AbstractSerializer implements Disposable, Serviceable
044{
045    /**
046     * The namespace for elements handled by this serializer,
047     * "http://apache.org/cocoon/zip-archive/1.0".
048     */
049    public static final String ZIP_NAMESPACE = "http://apache.org/cocoon/zip-archive/1.0";
050
051    private static final int START_STATE = 0;
052
053    private static final int IN_ZIP_STATE = 1;
054
055    private static final int IN_CONTENT_STATE = 2;
056
057    /** The component manager */
058    protected ServiceManager _manager;
059
060    /** The serializer component selector */
061    protected ServiceSelector _selector;
062
063    /** The Zip stream where entries will be written */
064    protected ZipArchiveOutputStream _zipOutput;
065
066    /** The current state */
067    protected int _state = START_STATE;
068
069    /** The resolver to get sources */
070    protected SourceResolver _resolver;
071
072    /** Temporary byte buffer to read source data */
073    protected byte[] _buffer;
074
075    /** Serializer used when in IN_CONTENT state */
076    protected Serializer _serializer;
077
078    /** Current depth of the serialized content */
079    protected int _contentDepth;
080
081    /** Used to collect namespaces */
082    private NamespaceSupport _nsSupport = new NamespaceSupport();
083
084    /**
085     * Store exception
086     */
087    private SAXException _exception;
088
089    @Override
090    public void service(ServiceManager manager) throws ServiceException
091    {
092        this._manager = manager;
093        this._resolver = (SourceResolver) this._manager.lookup(SourceResolver.ROLE);
094    }
095
096    /**
097     * Returns default mime type for zip archives, <code>application/zip</code>. Can be overridden
098     * in the sitemap.
099     * 
100     * @return application/zip
101     */
102    @Override
103    public String getMimeType()
104    {
105        return "application/zip";
106    }
107    
108    @Override
109    public void startDocument() throws SAXException
110    {
111        this._state = START_STATE;
112        this._zipOutput = new ZipArchiveOutputStream(this.output);
113        this._zipOutput.setCreateUnicodeExtraFields(UnicodeExtraFieldPolicy.ALWAYS);
114        this._zipOutput.setEncoding("UTF-8");
115    }
116
117    /**
118     * Begin the scope of a prefix-URI Namespace mapping.
119     * 
120     * @param prefix The Namespace prefix being declared.
121     * @param uri The Namespace URI the prefix is mapped to.
122     */
123    @Override
124    public void startPrefixMapping(String prefix, String uri) throws SAXException
125    {
126        if (_state == IN_CONTENT_STATE && this._contentDepth > 0)
127        {
128            // Pass to the serializer
129            super.startPrefixMapping(prefix, uri);
130
131        }
132        else
133        {
134            // Register it if it's not our own namespace (useless to content)
135            if (!uri.equals(ZIP_NAMESPACE))
136            {
137                this._nsSupport.declarePrefix(prefix, uri);
138            }
139        }
140    }
141    
142    @Override
143    public void endPrefixMapping(String prefix) throws SAXException
144    {
145        if (_state == IN_CONTENT_STATE && this._contentDepth > 0)
146        {
147            // Pass to the serializer
148            super.endPrefixMapping(prefix);
149        }
150    }
151
152    // Note : no need to implement endPrefixMapping() as we just need to pass it through if there
153    // is a serializer, which is what the superclass does.
154
155    @Override
156    public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException
157    {
158
159        // Damage control. Sometimes one exception is just not enough...
160        if (this._exception != null)
161        {
162            throw this._exception;
163        }
164
165        switch (_state)
166        {
167            case START_STATE:
168                // expecting "zip" as the first element
169                if (namespaceURI.equals(ZIP_NAMESPACE) && localName.equals("archive"))
170                {
171                    this._nsSupport.pushContext();
172                    this._state = IN_ZIP_STATE;
173                }
174                else
175                {
176                    this._exception = new SAXException("Expecting 'archive' root element (got '" + localName + "')");
177                    throw this._exception;
178                }
179                break;
180
181            case IN_ZIP_STATE:
182                // expecting "entry" element
183                if (namespaceURI.equals(ZIP_NAMESPACE) && localName.equals("entry"))
184                {
185                    this._nsSupport.pushContext();
186                    // Get the source
187                    addEntry(atts);
188                }
189                else
190                {
191                    this._exception = new SAXException("Expecting 'entry' element (got '" + localName + "')");
192                    throw this._exception;
193                }
194                break;
195
196            case IN_CONTENT_STATE:
197                if (this._contentDepth == 0)
198                {
199                    // Give it any namespaces already declared
200                    Enumeration prefixes = this._nsSupport.getPrefixes();
201                    while (prefixes.hasMoreElements())
202                    {
203                        String prefix = (String) prefixes.nextElement();
204                        super.startPrefixMapping(prefix, this._nsSupport.getURI(prefix));
205                    }
206                }
207
208                this._contentDepth++;
209                super.startElement(namespaceURI, localName, qName, atts);
210                break;
211            default:
212                break;
213        }
214    }
215
216    @Override
217    public void characters(char[] buffer, int offset, int length) throws SAXException
218    {
219        // Propagate text to the serializer only if we have encountered the content's top-level
220        // element. Otherwhise, the serializer may be confused by some characters occuring between
221        // startDocument() and the first startElement() (e.g. Batik fails hard in that case)
222        if (this._state == IN_CONTENT_STATE && this._contentDepth > 0)
223        {
224            super.characters(buffer, offset, length);
225        }
226    }
227
228    /**
229     * Add an entry in the archive.
230     * 
231     * @param atts the attributes that describe the entry
232     * @throws SAXException if an error occurred
233     */
234    protected void addEntry(Attributes atts) throws SAXException
235    {
236        String name = atts.getValue("name");
237        if (name == null)
238        {
239            this._exception = new SAXException("No name given to the Zip entry");
240            throw this._exception;
241        }
242
243        String src = atts.getValue("src");
244        String serializerType = atts.getValue("serializer");
245
246        if (src == null && serializerType == null)
247        {
248            this._exception = new SAXException("No source nor serializer given for the Zip entry '" + name + "'");
249            throw this._exception;
250        }
251
252        if (src != null && serializerType != null)
253        {
254            this._exception = new SAXException("Cannot specify both 'src' and 'serializer' on a Zip entry '" + name + "'");
255            throw this._exception;
256        }
257
258        Source source = null;
259        try
260        {
261            if (src != null)
262            {
263                // Get the source and its data
264                source = _resolver.resolveURI(src);
265                
266                try (InputStream sourceInput = source.getInputStream())
267                {
268                    // Create a new Zip entry with file modification time.
269                    ZipArchiveEntry entry = new ZipArchiveEntry(name);
270                    long lastModified = source.getLastModified();
271                    if (lastModified != 0)
272                    {
273                        entry.setTime(lastModified);
274                    }
275                    // this.zipOutput.putNextEntry(entry);
276                    this._zipOutput.putArchiveEntry(entry);
277
278                    // Buffer lazily allocated
279                    if (this._buffer == null)
280                    {
281                        this._buffer = new byte[8192];
282                    }
283
284                    // Copy the source to the zip
285                    int len;
286                    while ((len = sourceInput.read(this._buffer)) > 0)
287                    {
288                        this._zipOutput.write(this._buffer, 0, len);
289                    }
290
291                    // and close the entry
292                    // this.zipOutput.closeEntry();
293                    this._zipOutput.closeArchiveEntry();
294                }
295            }
296            else
297            {
298                // Create a new Zip entry with current time.
299                // ZipEntry entry = new ZipEntry(name);
300                ZipArchiveEntry entry = new ZipArchiveEntry(name);
301                // this.zipOutput.putNextEntry(entry);
302                this._zipOutput.putArchiveEntry(entry);
303
304                // Serialize content
305                if (this._selector == null)
306                {
307                    this._selector = (ServiceSelector) this._manager.lookup(Serializer.ROLE + "Selector");
308                }
309
310                // Get the serializer
311                this._serializer = (Serializer) this._selector.select(serializerType);
312
313                // Direct its output to the zip file, filtering calls to close()
314                // (we don't want the archive to be closed by the serializer)
315                this._serializer.setOutputStream(new FilterOutputStream(this._zipOutput)
316                {
317                    @Override
318                    public void close()
319                    {
320                        // 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        }
335        catch (RuntimeException re)
336        {
337            throw re;
338        }
339        catch (SAXException se)
340        {
341            this._exception = se;
342            throw this._exception;
343        }
344        catch (Exception e)
345        {
346            this._exception = new SAXException(e);
347            throw this._exception;
348        }
349        finally
350        {
351            this._resolver.release(source);
352        }
353    }
354
355    @Override
356    public void endElement(String namespaceURI, String localName, String qName) throws SAXException
357    {
358
359        // Damage control. Sometimes one exception is just not enough...
360        if (this._exception != null)
361        {
362            throw this._exception;
363        }
364
365        if (_state == IN_CONTENT_STATE)
366        {
367            super.endElement(namespaceURI, localName, qName);
368            this._contentDepth--;
369
370            if (this._contentDepth == 0)
371            {
372                // End of this entry
373
374                // close all declared namespaces.
375                Enumeration prefixes = this._nsSupport.getPrefixes();
376                while (prefixes.hasMoreElements())
377                {
378                    String prefix = (String) prefixes.nextElement();
379                    super.endPrefixMapping(prefix);
380                }
381
382                super.endDocument();
383
384                try
385                {
386                    // this.zipOutput.closeEntry();
387                    this._zipOutput.closeArchiveEntry();
388                }
389                catch (IOException ioe)
390                {
391                    this._exception = new SAXException(ioe);
392                    throw this._exception;
393                }
394
395                super.setConsumer(null);
396                this._selector.release(this._serializer);
397                this._serializer = null;
398
399                // Go back to listening for entries
400                this._state = IN_ZIP_STATE;
401            }
402        }
403        else
404        {
405            this._nsSupport.popContext();
406        }
407    }
408
409    @Override
410    public void endDocument() throws SAXException
411    {
412        try
413        {
414            // Close the zip archive
415            this._zipOutput.finish();
416
417        }
418        catch (IOException ioe)
419        {
420            throw new SAXException(ioe);
421        }
422    }
423
424    @Override
425    public void recycle()
426    {
427        this._exception = null;
428        if (this._serializer != null)
429        {
430            this._selector.release(this._serializer);
431        }
432        if (this._selector != null)
433        {
434            this._manager.release(this._selector);
435        }
436
437        this._nsSupport.reset();
438        super.recycle();
439    }
440
441    @Override
442    public void dispose()
443    {
444        if (this._manager != null)
445        {
446            this._manager.release(this._resolver);
447            this._resolver = null;
448            this._manager = null;
449        }
450    }
451
452}