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.File; 019import java.io.FileInputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.OutputStream; 023import java.io.Serializable; 024import java.net.URI; 025import java.util.HashMap; 026import java.util.Map; 027 028import org.apache.avalon.framework.activity.Disposable; 029import org.apache.avalon.framework.configuration.Configurable; 030import org.apache.avalon.framework.configuration.Configuration; 031import org.apache.avalon.framework.configuration.ConfigurationException; 032import org.apache.avalon.framework.context.ContextException; 033import org.apache.avalon.framework.context.Contextualizable; 034import org.apache.avalon.framework.service.ServiceException; 035import org.apache.avalon.framework.service.ServiceManager; 036import org.apache.avalon.framework.service.Serviceable; 037import org.apache.cocoon.Constants; 038import org.apache.cocoon.caching.CacheableProcessingComponent; 039import org.apache.cocoon.environment.Context; 040import org.apache.cocoon.serialization.AbstractSerializer; 041import org.apache.excalibur.source.Source; 042import org.apache.excalibur.source.SourceNotFoundException; 043import org.apache.excalibur.source.SourceResolver; 044import org.apache.excalibur.source.SourceValidity; 045import org.apache.excalibur.source.impl.validity.NOPValidity; 046import org.apache.fop.apps.FOPException; 047import org.apache.fop.apps.FOUserAgent; 048import org.apache.fop.apps.Fop; 049import org.apache.fop.apps.FopConfParser; 050import org.apache.fop.apps.FopFactory; 051import org.apache.fop.apps.FopFactoryBuilder; 052import org.apache.xmlgraphics.io.Resource; 053import org.apache.xmlgraphics.io.ResourceResolver; 054 055/** 056 * FOP 0.95 (and newer) based serializer. 057 */ 058public class FOPNGSerializer extends AbstractSerializer implements Configurable, CacheableProcessingComponent, Serviceable, ResourceResolver, Disposable, Contextualizable 059{ 060 /** The source resolver */ 061 protected SourceResolver _resolver; 062 063 /** 064 * Factory to create fop objects 065 */ 066 protected FopFactory _fopfactory; 067 068 /** 069 * The FOP instance. 070 */ 071 protected Fop _fop; 072 073 /** 074 * The current <code>mime-type</code>. 075 */ 076 protected String _mimetype; 077 078 /** 079 * Should we set the content length ? 080 */ 081 protected boolean _setContentLength = true; 082 083 /** 084 * Manager to get URLFactory from. 085 */ 086 protected ServiceManager _manager; 087 088 private Map<String, String> _rendererOptions; 089 090 private Context _context; 091 092 public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException 093 { 094 _context = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 095 } 096 097 /** 098 * Set the component manager for this serializer. 099 */ 100 public void service(ServiceManager smanager) throws ServiceException 101 { 102 this._manager = smanager; 103 this._resolver = (SourceResolver) this._manager.lookup(SourceResolver.ROLE); 104 } 105 106 /** 107 * Set the configurations for this serializer. 108 */ 109 public void configure(Configuration conf) throws ConfigurationException 110 { 111 // should the content length be set 112 this._setContentLength = conf.getChild("set-content-length").getValueAsBoolean(true); 113 114 File base = new File(_context.getRealPath("/")); 115 URI baseURI = base.toURI(); 116 FopFactoryBuilder fopFactoryBuilder = null; 117 118 String configUrl = conf.getChild("user-config").getValue("context://WEB-INF/param/fop-user-config.xml"); 119 if (configUrl != null) 120 { 121 Source configSource = null; 122 SourceResolver resolver = null; 123 try 124 { 125 resolver = (SourceResolver) this._manager.lookup(SourceResolver.ROLE); 126 configSource = resolver.resolveURI(configUrl); 127 if (configSource.exists()) 128 { 129 if (getLogger().isDebugEnabled()) 130 { 131 getLogger().debug("Loading configuration from " + configSource.getURI()); 132 } 133 134 try (InputStream is = configSource.getInputStream()) 135 { 136 FopConfParser confParser = new FopConfParser(is, baseURI, this); 137 fopFactoryBuilder = confParser.getFopFactoryBuilder(); 138 } 139 } 140 else 141 { 142 throw new SourceNotFoundException("There is no configuration file " + configSource.getURI()); 143 } 144 } 145 catch (SourceNotFoundException e) 146 { 147 if (getLogger().isDebugEnabled()) 148 { 149 getLogger().debug("There is no configuration file " + configUrl, e); 150 } 151 } 152 catch (Exception e) 153 { 154 getLogger().warn("Cannot load configuration from " + configUrl); 155 throw new ConfigurationException("Cannot load configuration from " + configUrl, e); 156 } 157 finally 158 { 159 if (resolver != null) 160 { 161 resolver.release(configSource); 162 _manager.release(resolver); 163 } 164 } 165 } 166 167 if (fopFactoryBuilder == null) 168 { 169 fopFactoryBuilder = new FopFactoryBuilder(baseURI, this); 170 } 171 172 _fopfactory = fopFactoryBuilder.build(); 173 174 // Get the mime type. 175 this._mimetype = conf.getAttribute("mime-type"); 176 177 Configuration confRenderer = conf.getChild("renderer-config"); 178 if (confRenderer != null) 179 { 180 Configuration[] parameters = confRenderer.getChildren("parameter"); 181 if (parameters.length > 0) 182 { 183 _rendererOptions = new HashMap<>(); 184 for (int i = 0; i < parameters.length; i++) 185 { 186 String name = parameters[i].getAttribute("name"); 187 String value = parameters[i].getAttribute("value"); 188 _rendererOptions.put(name, value); 189 if (getLogger().isDebugEnabled()) 190 { 191 getLogger().debug("renderer " + String.valueOf(name) + " = " + String.valueOf(value)); 192 } 193 } 194 } 195 } 196 } 197 198 /** 199 * Recycle serializer by removing references 200 */ 201 @Override 202 public void recycle() 203 { 204 super.recycle(); 205 this._fop = null; 206 } 207 208 public void dispose() 209 { 210 if (this._resolver != null) 211 { 212 this._manager.release(this._resolver); 213 this._resolver = null; 214 } 215 this._manager = null; 216 } 217 218 // ----------------------------------------------------------------- 219 220 /** 221 * Return the MIME type. 222 */ 223 @Override 224 public String getMimeType() 225 { 226 return _mimetype; 227 } 228 229 @Override 230 public void setOutputStream(OutputStream out) throws IOException 231 { 232 233 // Give the source resolver to Batik which is used by FOP 234 // SourceProtocolHandler.setup(this.resolver); 235 236 FOUserAgent userAgent = _fopfactory.newFOUserAgent(); 237 if (this._rendererOptions != null) 238 { 239 userAgent.getRendererOptions().putAll(this._rendererOptions); 240 } 241 userAgent.setAccessibility(true); 242 try 243 { 244 this._fop = _fopfactory.newFop(getMimeType(), userAgent, out); 245 setContentHandler(this._fop.getDefaultHandler()); 246 } 247 catch (FOPException e) 248 { 249 getLogger().error("FOP setup failed", e); 250 throw new IOException("Unable to setup fop: " + e.getLocalizedMessage()); 251 } 252 } 253 254 /** 255 * Generate the unique key. This key must be unique inside the space of this 256 * component. This method must be invoked before the generateValidity() 257 * method. 258 * 259 * @return The generated key or <code>0</code> if the component is currently 260 * not cacheable. 261 */ 262 public Serializable getKey() 263 { 264 return "1"; 265 } 266 267 /** 268 * Generate the validity object. Before this method can be invoked the 269 * generateKey() method must be invoked. 270 * 271 * @return The generated validity object or <code>null</code> if the 272 * component is currently not cacheable. 273 */ 274 public SourceValidity getValidity() 275 { 276 return NOPValidity.SHARED_INSTANCE; 277 } 278 279 /** 280 * Test if the component wants to set the content length 281 */ 282 @Override 283 public boolean shouldSetContentLength() 284 { 285 return this._setContentLength; 286 } 287 288 public Resource getResource(URI uri) throws IOException 289 { 290 String href = uri.toString(); 291 292 if (href.indexOf(':') != -1) 293 { 294 Source source = _resolver.resolveURI(href); 295 return new Resource(source.getMimeType(), source.getInputStream()); 296 } 297 else 298 { 299 String base = _context.getRealPath("/"); 300 301 if (href.startsWith("/")) 302 { 303 href = href.substring(1); 304 } 305 306 File source = new File(base, href); 307 return new Resource(new FileInputStream(source)); 308 } 309 } 310 311 public OutputStream getOutputStream(URI uri) throws IOException 312 { 313 throw new UnsupportedOperationException("getOutputStream"); 314 } 315 316 /** 317 * An InputStream which releases the Cocoon/Avalon source from which the 318 * InputStream has been retrieved when the stream is closed. 319 */ 320 public static final class ReleaseSourceInputStream extends InputStream 321 { 322 private InputStream _delegate; 323 324 private Source _source; 325 326 private SourceResolver _sourceResolver; 327 328 ReleaseSourceInputStream(InputStream delegate, Source source, SourceResolver sourceResolver) 329 { 330 this._delegate = delegate; 331 this._source = source; 332 this._sourceResolver = sourceResolver; 333 } 334 335 @Override 336 public void close() throws IOException 337 { 338 _delegate.close(); 339 _sourceResolver.release(_source); 340 } 341 342 @Override 343 public int read() throws IOException 344 { 345 return _delegate.read(); 346 } 347 348 @Override 349 public int read(byte[] b) throws IOException 350 { 351 return _delegate.read(b); 352 } 353 354 @Override 355 public int read(byte[] b, int off, int len) throws IOException 356 { 357 return _delegate.read(b, off, len); 358 } 359 360 @Override 361 public long skip(long n) throws IOException 362 { 363 return _delegate.skip(n); 364 } 365 366 @Override 367 public int available() throws IOException 368 { 369 return _delegate.available(); 370 } 371 372 @Override 373 public synchronized void mark(int readlimit) 374 { 375 _delegate.mark(readlimit); 376 } 377 378 @Override 379 public synchronized void reset() throws IOException 380 { 381 _delegate.reset(); 382 } 383 384 @Override 385 public boolean markSupported() 386 { 387 return _delegate.markSupported(); 388 } 389 } 390}