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.configuration.SAXConfigurationHandler;
033import org.apache.avalon.framework.context.ContextException;
034import org.apache.avalon.framework.context.Contextualizable;
035import org.apache.avalon.framework.service.ServiceException;
036import org.apache.avalon.framework.service.ServiceManager;
037import org.apache.avalon.framework.service.Serviceable;
038import org.apache.cocoon.Constants;
039import org.apache.cocoon.caching.CacheableProcessingComponent;
040import org.apache.cocoon.components.source.SourceUtil;
041import org.apache.cocoon.environment.Context;
042import org.apache.cocoon.serialization.AbstractSerializer;
043import org.apache.excalibur.source.Source;
044import org.apache.excalibur.source.SourceNotFoundException;
045import org.apache.excalibur.source.SourceResolver;
046import org.apache.excalibur.source.SourceValidity;
047import org.apache.excalibur.source.impl.validity.NOPValidity;
048import org.apache.fop.apps.FOPException;
049import org.apache.fop.apps.FOUserAgent;
050import org.apache.fop.apps.Fop;
051import org.apache.fop.apps.FopFactory;
052import org.apache.fop.apps.FopFactoryBuilder;
053import org.apache.xmlgraphics.io.Resource;
054import org.apache.xmlgraphics.io.ResourceResolver;
055
056/**
057 * FOP 0.95 (and newer) based serializer.
058 */
059public class FOPNGSerializer extends AbstractSerializer implements Configurable, CacheableProcessingComponent, Serviceable, ResourceResolver, Disposable, Contextualizable
060{
061    /** The source resolver */
062    protected SourceResolver _resolver;
063
064    /**
065     * Factory to create fop objects
066     */
067    protected FopFactory _fopfactory;
068
069    /**
070     * The FOP instance.
071     */
072    protected Fop _fop;
073
074    /**
075     * The current <code>mime-type</code>.
076     */
077    protected String _mimetype;
078
079    /**
080     * Should we set the content length ?
081     */
082    protected boolean _setContentLength = true;
083
084    /**
085     * Manager to get URLFactory from.
086     */
087    protected ServiceManager _manager;
088
089    private Map<String, String> _rendererOptions;
090    
091    private Context _context;
092    
093    public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException
094    {
095        _context = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
096    }
097
098    /**
099     * Set the component manager for this serializer.
100     */
101    public void service(ServiceManager smanager) throws ServiceException
102    {
103        this._manager = smanager;
104        this._resolver = (SourceResolver) this._manager.lookup(SourceResolver.ROLE);
105    }
106
107    /**
108     * Set the configurations for this serializer.
109     */
110    public void configure(Configuration conf) throws ConfigurationException
111    {
112        // should the content length be set
113        this._setContentLength = conf.getChild("set-content-length").getValueAsBoolean(true);
114
115        Configuration config = null;
116        String configUrl = conf.getChild("user-config").getValue("context://WEB-INF/param/fop-user-config.xml");
117        if (configUrl != null)
118        {
119            Source configSource = null;
120            SourceResolver resolver = null;
121            try
122            {
123                resolver = (SourceResolver) this._manager.lookup(SourceResolver.ROLE);
124                configSource = resolver.resolveURI(configUrl);
125                if (configSource.exists())
126                {
127                    if (getLogger().isDebugEnabled())
128                    {
129                        getLogger().debug("Loading configuration from " + configSource.getURI());
130                    }
131                    SAXConfigurationHandler configHandler = new SAXConfigurationHandler();
132                    SourceUtil.toSAX(configSource, configHandler);
133                    
134                    config = configHandler.getConfiguration();
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}