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