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.FopFactory; 050import org.apache.fop.apps.FopFactoryBuilder; 051import org.apache.fop.configuration.DefaultConfigurationBuilder; 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 org.apache.fop.configuration.Configuration config = null; 115 String configUrl = conf.getChild("user-config").getValue("context://WEB-INF/param/fop-user-config.xml"); 116 if (configUrl != null) 117 { 118 Source configSource = null; 119 SourceResolver resolver = null; 120 try 121 { 122 resolver = (SourceResolver) this._manager.lookup(SourceResolver.ROLE); 123 configSource = resolver.resolveURI(configUrl); 124 if (configSource.exists()) 125 { 126 if (getLogger().isDebugEnabled()) 127 { 128 getLogger().debug("Loading configuration from " + configSource.getURI()); 129 } 130 131 try (InputStream is = configSource.getInputStream()) 132 { 133 config = new DefaultConfigurationBuilder().build(is); 134 } 135 } 136 else 137 { 138 throw new SourceNotFoundException("There is no configuration file " + configSource.getURI()); 139 } 140 } 141 catch (SourceNotFoundException e) 142 { 143 if (getLogger().isDebugEnabled()) 144 { 145 getLogger().debug("There is no configuration file " + configUrl, e); 146 } 147 } 148 catch (Exception e) 149 { 150 getLogger().warn("Cannot load configuration from " + configUrl); 151 throw new ConfigurationException("Cannot load configuration from " + configUrl, e); 152 } 153 finally 154 { 155 if (resolver != null) 156 { 157 resolver.release(configSource); 158 _manager.release(resolver); 159 } 160 } 161 } 162 163 File base = new File(_context.getRealPath("/")); 164 FopFactoryBuilder fopFactoryBuilder = new FopFactoryBuilder(base.toURI(), this); 165 166 if (config != null) 167 { 168 fopFactoryBuilder.setConfiguration(config); 169 } 170 171 _fopfactory = fopFactoryBuilder.build(); 172 173 // Get the mime type. 174 this._mimetype = conf.getAttribute("mime-type"); 175 176 Configuration confRenderer = conf.getChild("renderer-config"); 177 if (confRenderer != null) 178 { 179 Configuration[] parameters = confRenderer.getChildren("parameter"); 180 if (parameters.length > 0) 181 { 182 _rendererOptions = new HashMap<>(); 183 for (int i = 0; i < parameters.length; i++) 184 { 185 String name = parameters[i].getAttribute("name"); 186 String value = parameters[i].getAttribute("value"); 187 _rendererOptions.put(name, value); 188 if (getLogger().isDebugEnabled()) 189 { 190 getLogger().debug("renderer " + String.valueOf(name) + " = " + String.valueOf(value)); 191 } 192 } 193 } 194 } 195 } 196 197 /** 198 * Recycle serializer by removing references 199 */ 200 @Override 201 public void recycle() 202 { 203 super.recycle(); 204 this._fop = null; 205 } 206 207 public void dispose() 208 { 209 if (this._resolver != null) 210 { 211 this._manager.release(this._resolver); 212 this._resolver = null; 213 } 214 this._manager = null; 215 } 216 217 // ----------------------------------------------------------------- 218 219 /** 220 * Return the MIME type. 221 */ 222 @Override 223 public String getMimeType() 224 { 225 return _mimetype; 226 } 227 228 @Override 229 public void setOutputStream(OutputStream out) throws IOException 230 { 231 232 // Give the source resolver to Batik which is used by FOP 233 // SourceProtocolHandler.setup(this.resolver); 234 235 FOUserAgent userAgent = _fopfactory.newFOUserAgent(); 236 if (this._rendererOptions != null) 237 { 238 userAgent.getRendererOptions().putAll(this._rendererOptions); 239 } 240 userAgent.setAccessibility(true); 241 try 242 { 243 this._fop = _fopfactory.newFop(getMimeType(), userAgent, out); 244 setContentHandler(this._fop.getDefaultHandler()); 245 } 246 catch (FOPException e) 247 { 248 getLogger().error("FOP setup failed", e); 249 throw new IOException("Unable to setup fop: " + e.getLocalizedMessage()); 250 } 251 } 252 253 /** 254 * Generate the unique key. This key must be unique inside the space of this 255 * component. This method must be invoked before the generateValidity() 256 * method. 257 * 258 * @return The generated key or <code>0</code> if the component is currently 259 * not cacheable. 260 */ 261 public Serializable getKey() 262 { 263 return "1"; 264 } 265 266 /** 267 * Generate the validity object. Before this method can be invoked the 268 * generateKey() method must be invoked. 269 * 270 * @return The generated validity object or <code>null</code> if the 271 * component is currently not cacheable. 272 */ 273 public SourceValidity getValidity() 274 { 275 return NOPValidity.SHARED_INSTANCE; 276 } 277 278 /** 279 * Test if the component wants to set the content length 280 */ 281 @Override 282 public boolean shouldSetContentLength() 283 { 284 return this._setContentLength; 285 } 286 287 public Resource getResource(URI uri) throws IOException 288 { 289 String href = uri.toString(); 290 291 if (href.indexOf(':') != -1) 292 { 293 Source source = _resolver.resolveURI(href); 294 return new Resource(source.getMimeType(), source.getInputStream()); 295 } 296 else 297 { 298 String base = _context.getRealPath("/"); 299 300 if (href.startsWith("/")) 301 { 302 href = href.substring(1); 303 } 304 305 File source = new File(base, href); 306 return new Resource(new FileInputStream(source)); 307 } 308 } 309 310 public OutputStream getOutputStream(URI uri) throws IOException 311 { 312 throw new UnsupportedOperationException("getOutputStream"); 313 } 314 315 /** 316 * An InputStream which releases the Cocoon/Avalon source from which the 317 * InputStream has been retrieved when the stream is closed. 318 */ 319 public static final class ReleaseSourceInputStream extends InputStream 320 { 321 private InputStream _delegate; 322 323 private Source _source; 324 325 private SourceResolver _sourceResolver; 326 327 ReleaseSourceInputStream(InputStream delegate, Source source, SourceResolver sourceResolver) 328 { 329 this._delegate = delegate; 330 this._source = source; 331 this._sourceResolver = sourceResolver; 332 } 333 334 @Override 335 public void close() throws IOException 336 { 337 _delegate.close(); 338 _sourceResolver.release(_source); 339 } 340 341 @Override 342 public int read() throws IOException 343 { 344 return _delegate.read(); 345 } 346 347 @Override 348 public int read(byte[] b) throws IOException 349 { 350 return _delegate.read(b); 351 } 352 353 @Override 354 public int read(byte[] b, int off, int len) throws IOException 355 { 356 return _delegate.read(b, off, len); 357 } 358 359 @Override 360 public long skip(long n) throws IOException 361 { 362 return _delegate.skip(n); 363 } 364 365 @Override 366 public int available() throws IOException 367 { 368 return _delegate.available(); 369 } 370 371 @Override 372 public synchronized void mark(int readlimit) 373 { 374 _delegate.mark(readlimit); 375 } 376 377 @Override 378 public synchronized void reset() throws IOException 379 { 380 _delegate.reset(); 381 } 382 383 @Override 384 public boolean markSupported() 385 { 386 return _delegate.markSupported(); 387 } 388 } 389}