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}