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}