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}