/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.cocoon.serialization; import org.apache.avalon.framework.activity.Disposable; import org.apache.avalon.framework.configuration.Configurable; import org.apache.avalon.framework.configuration.Configuration; import org.apache.avalon.framework.configuration.ConfigurationException; import org.apache.avalon.framework.configuration.SAXConfigurationHandler; import org.apache.avalon.framework.service.ServiceException; import org.apache.avalon.framework.service.ServiceManager; import org.apache.avalon.framework.service.Serviceable; import org.apache.cocoon.caching.CacheableProcessingComponent; import org.apache.cocoon.components.source.SourceUtil; import org.apache.excalibur.source.Source; import org.apache.excalibur.source.SourceException; import org.apache.excalibur.source.SourceResolver; import org.apache.excalibur.source.SourceValidity; import org.apache.excalibur.source.impl.validity.NOPValidity; import org.apache.fop.apps.FOPException; import org.apache.fop.apps.FOUserAgent; import org.apache.fop.apps.Fop; import org.apache.fop.apps.FopFactory; import javax.xml.transform.TransformerException; import javax.xml.transform.URIResolver; import javax.xml.transform.stream.StreamSource; import java.io.*; import java.util.HashMap; import java.util.Map; /** * FOP 0.93 (and newer) based serializer. * * @version $Id$ */ public class FOPSerializer extends AbstractSerializer implements Configurable, CacheableProcessingComponent, Serviceable, URIResolver, Disposable { protected SourceResolver resolver; /** * Factory to create fop objects */ protected FopFactory fopfactory = FopFactory.newInstance(); /** * The FOP instance. */ protected Fop fop; /** * The current <code>mime-type</code>. */ protected String mimetype; /** * Should we set the content length ? */ protected boolean setContentLength = true; /** * Manager to get URLFactory from. */ protected ServiceManager manager; private Map rendererOptions; /** * Set the component manager for this serializer. */ public void service(ServiceManager manager) throws ServiceException { this.manager = manager; this.resolver = (SourceResolver) this.manager.lookup(SourceResolver.ROLE); } /** * Set the configurations for this serializer. */ public void configure(Configuration conf) throws ConfigurationException { //should the content length be set this.setContentLength = conf.getChild("set-content-length").getValueAsBoolean(true); String configUrl = conf.getChild("user-config").getValue(null); if (configUrl != null) { Source configSource = null; SourceResolver resolver = null; try { resolver = (SourceResolver)this.manager.lookup(SourceResolver.ROLE); configSource = resolver.resolveURI(configUrl); if (getLogger().isDebugEnabled()) { getLogger().debug("Loading configuration from " + configSource.getURI()); } SAXConfigurationHandler configHandler = new SAXConfigurationHandler(); SourceUtil.toSAX(configSource, configHandler); fopfactory.setUserConfig(configHandler.getConfiguration()); } catch (Exception e) { getLogger().warn("Cannot load configuration from " + configUrl); throw new ConfigurationException("Cannot load configuration from " + configUrl, e); } finally { if (resolver != null) { resolver.release(configSource); manager.release(resolver); } } } fopfactory.setURIResolver(this); // Get the mime type. this.mimetype = conf.getAttribute("mime-type"); Configuration confRenderer = conf.getChild("renderer-config"); if (confRenderer != null) { Configuration[] parameters = confRenderer.getChildren("parameter"); if (parameters.length > 0) { rendererOptions = new HashMap(); for (int i = 0; i < parameters.length; i++) { String name = parameters[i].getAttribute("name"); String value = parameters[i].getAttribute("value"); if (getLogger().isDebugEnabled()) { getLogger().debug("renderer " + String.valueOf(name) + " = " + String.valueOf(value)); } } } } } /** * Recycle serializer by removing references */ public void recycle() { super.recycle(); this.fop = null; } public void dispose() { if (this.resolver != null) { this.manager.release(this.resolver); this.resolver = null; } this.manager = null; } // ----------------------------------------------------------------- /** * Return the MIME type. */ public String getMimeType() { return mimetype; } /** * Create the FOP driver * Set the <code>OutputStream</code> where the XML should be serialized. * @throws IOException */ public void setOutputStream(OutputStream out) throws IOException { // Give the source resolver to Batik which is used by FOP //SourceProtocolHandler.setup(this.resolver); FOUserAgent userAgent = fopfactory.newFOUserAgent(); if (this.rendererOptions != null) { userAgent.getRendererOptions().putAll(this.rendererOptions); } try { this.fop = fopfactory.newFop(getMimeType(), userAgent, out); setContentHandler(this.fop.getDefaultHandler()); } catch (FOPException e) { getLogger().error("FOP setup failed", e); throw new IOException("Unable to setup fop: " + e.getLocalizedMessage()); } } /** * Generate the unique key. * This key must be unique inside the space of this component. * This method must be invoked before the generateValidity() method. * * @return The generated key or <code>0</code> if the component * is currently not cacheable. */ public Serializable getKey() { return "1"; } /** * Generate the validity object. * Before this method can be invoked the generateKey() method * must be invoked. * * @return The generated validity object or <code>null</code> if the * component is currently not cacheable. */ public SourceValidity getValidity() { return NOPValidity.SHARED_INSTANCE; } /** * Test if the component wants to set the content length */ public boolean shouldSetContentLength() { return this.setContentLength; } //From URIResolver, copied from TraxProcessor public javax.xml.transform.Source resolve(String href, String base) throws TransformerException { if (getLogger().isDebugEnabled()) { getLogger().debug("resolve(href = " + href + ", base = " + base + "); resolver = " + resolver); } StreamSource streamSource = null; Source source = null; try { if (base == null || href.indexOf(":") > 1) { // Null base - href must be an absolute URL source = resolver.resolveURI(href); } else if (href.length() == 0) { // Empty href resolves to base source = resolver.resolveURI(base); } else { // is the base a file or a real m_url if (!base.startsWith("file:")) { int lastPathElementPos = base.lastIndexOf('/'); if (lastPathElementPos == -1) { // this should never occur as the base should // always be protocol:/.... return null; // we can't resolve this } else { source = resolver.resolveURI(base.substring(0, lastPathElementPos) + "/" + href); } } else { File parent = new File(base.substring(5)); File parent2 = new File(parent.getParentFile(), href); source = resolver.resolveURI(parent2.toURI().toURL().toExternalForm()); } } if (getLogger().isDebugEnabled()) { getLogger().debug("source = " + source + ", system id = " + source.getURI()); } streamSource = new StreamSource(new ReleaseSourceInputStream(source.getInputStream(), source, resolver), source.getURI()); } catch (SourceException e) { if (getLogger().isDebugEnabled()) { getLogger().debug("Failed to resolve " + href + "(base = " + base + "), return null", e); } // CZ: To obtain the same behaviour as when the resource is // transformed by the XSLT Transformer we should return null here. return null; } catch (java.net.MalformedURLException mue) { if (getLogger().isDebugEnabled()) { getLogger().debug("Failed to resolve " + href + "(base = " + base + "), return null", mue); } return null; } catch (IOException ioe) { if (getLogger().isDebugEnabled()) { getLogger().debug("Failed to resolve " + href + "(base = " + base + "), return null", ioe); } return null; } finally { // If streamSource is not null, the source should only be released when the input stream // is not needed anymore. if (streamSource == null) resolver.release(source); } return streamSource; } /** * An InputStream which releases the Cocoon/Avalon source from which the InputStream * has been retrieved when the stream is closed. */ public static class ReleaseSourceInputStream extends InputStream { private InputStream delegate; private Source source; private SourceResolver sourceResolver; private ReleaseSourceInputStream(InputStream delegate, Source source, SourceResolver sourceResolver) { this.delegate = delegate; this.source = source; this.sourceResolver = sourceResolver; } public void close() throws IOException { delegate.close(); sourceResolver.release(source); } public int read() throws IOException { return delegate.read(); } public int read(byte b[]) throws IOException { return delegate.read(b); } public int read(byte b[], int off, int len) throws IOException { return delegate.read(b, off, len); } public long skip(long n) throws IOException { return delegate.skip(n); } public int available() throws IOException { return delegate.available(); } public synchronized void mark(int readlimit) { delegate.mark(readlimit); } public synchronized void reset() throws IOException { delegate.reset(); } public boolean markSupported() { return delegate.markSupported(); } } }