/** * Copyright (C) 2009 Orbeon, Inc. * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU Lesser General Public License as published by the Free Software Foundation; either version * 2.1 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html */ package org.orbeon.oxf.processor.serializer; import org.apache.log4j.Logger; import org.orbeon.dom.Element; import org.orbeon.oxf.common.OXFException; import org.orbeon.oxf.externalcontext.ExternalContext; import org.orbeon.oxf.externalcontext.ResponseWrapper; import org.orbeon.oxf.http.StatusCode; import org.orbeon.oxf.pipeline.api.PipelineContext; import org.orbeon.oxf.processor.CacheableInputReader; import org.orbeon.oxf.processor.ProcessorInput; import org.orbeon.oxf.processor.ProcessorInputOutputInfo; import org.orbeon.oxf.processor.ProcessorUtils; import org.orbeon.oxf.processor.serializer.store.ResultStoreOutputStream; import org.orbeon.oxf.util.ContentTypes; import org.orbeon.oxf.util.LoggerFactory; import org.orbeon.oxf.util.NetUtils; import org.orbeon.oxf.util.URLRewriterUtils; import org.orbeon.oxf.xml.XPathUtils; import java.io.IOException; import java.io.OutputStream; import java.util.*; /** * Base class for all HTTP serializers. */ public abstract class HttpSerializerBase extends CachedSerializer { protected static final int DEFAULT_STATUS_CODE = StatusCode.Ok(); private static final boolean DEFAULT_FORCE_CONTENT_TYPE = false; private static final boolean DEFAULT_IGNORE_DOCUMENT_CONTENT_TYPE = false; private static final boolean DEFAULT_FORCE_ENCODING = false; private static final boolean DEFAULT_IGNORE_DOCUMENT_ENCODING = false; private static Logger logger = LoggerFactory.createLogger(HttpSerializerBase.class); protected HttpSerializerBase() { addInputInfo(new ProcessorInputOutputInfo(INPUT_CONFIG, getConfigSchemaNamespaceURI())); } /** * Return the default content type for this serializer. Must be overridden by subclasses. */ protected abstract String getDefaultContentType(); /** * Return the namespace URI of the schema validating the config input. Can be overridden by * subclasses. */ protected String getConfigSchemaNamespaceURI() { return SERIALIZER_CONFIG_NAMESPACE_URI; } public void start(PipelineContext pipelineContext) { try { // Read configuration input final Config config = readConfig(pipelineContext); // Get data input information final ProcessorInput dataInput = getInputByName(INPUT_DATA); ExternalContext externalContext = (ExternalContext) pipelineContext.getAttribute(PipelineContext.EXTERNAL_CONTEXT); final ExternalContext.Response response = externalContext.getResponse(); try { // Compute headers if (externalContext != null) { // Send an error if needed and return immediately int errorCode = config.errorCode; if (errorCode != DEFAULT_ERROR_CODE) { response.sendError(errorCode); return; } // Get last modification date and compute last modified if possible // NOTE: It is not clear if this is right! We had a discussion to "remove serializer last modified, // and use oxf:request-generator default validity". final long lastModified = findInputLastModified(pipelineContext, dataInput, false); // Set caching headers and force revalidation response.setPageCaching(lastModified); // Check if we are processing a forward. If so, we cannot tell the client that the content has not been modified. final boolean isForward = URLRewriterUtils.isForwarded(externalContext.getRequest()); if (!isForward) { // Check If-Modified-Since (conditional GET) and don't return content if condition is met if (!response.checkIfModifiedSince(externalContext.getRequest(), lastModified)) { response.setStatus(StatusCode.NotModified()); if (logger.isDebugEnabled()) logger.debug("Sending SC_NOT_MODIFIED"); return; } } // STATUS CODE: Set status code based on the configuration // An XML processing instruction may override this when the input is being read response.setStatus(config.statusCode); // Set custom headers if (config.headers != null) { for (Iterator<String> i = config.headers.iterator(); i.hasNext();) { String name = i.next(); String value = i.next(); response.setHeader(name, value); } } } // If we have an empty body, return w/o reading the data input if (config.empty) return; final OutputStream httpOutputStream = response.getOutputStream(); // If local caching of the data is enabled and if the configuration status code is a success code, use // the caching API. It doesn't make sense in HTTP to allow caching of non-successful responses. if (config.cacheUseLocalCache && NetUtils.isSuccessCode(config.statusCode)) { // We return a ResultStore final boolean[] read = new boolean[1]; final ExtendedResultStoreOutputStream resultStore = readCacheInputAsObject(pipelineContext, dataInput, new CacheableInputReader<ExtendedResultStoreOutputStream>() { private int statusCode = config.statusCode; public ExtendedResultStoreOutputStream read(PipelineContext pipelineContext, ProcessorInput input) { read[0] = true; if (logger.isDebugEnabled()) logger.debug("Output not cached"); try { final ExtendedResultStoreOutputStream resultStoreOutputStream = new ExtendedResultStoreOutputStream(httpOutputStream); // NOTE: readInput will call response.setContentType() and other methods so we intercept and save the // values. readInput(pipelineContext, new ResponseWrapper(response) { @Override public void setContentType(String contentType) { resultStoreOutputStream.setContentType(contentType); super.setContentType(contentType); } @Override public void setHeader(String name, String value) { resultStoreOutputStream.setHeader(name, value); super.setHeader(name, value); } @Override public OutputStream getOutputStream() { return resultStoreOutputStream; } @Override public void setStatus(int status) { // STATUS CODE: This typically is overridden via a processing instruction. statusCode = status; resultStoreOutputStream.setStatus(status); super.setStatus(status); } }, input, config); resultStoreOutputStream.close(); return resultStoreOutputStream; } catch (IOException e) { throw new OXFException(e); } } @Override public boolean allowCaching() { // It doesn't make sense in HTTP to allow caching of non-successful responses return NetUtils.isSuccessCode(statusCode); } }); // If the output was obtained from the cache, just write it if (!read[0]) { if (logger.isDebugEnabled()) logger.debug("Serializer output cached"); if (externalContext != null) { // Set saved status code final int status = resultStore.getStatus(); if (status > 0) response.setStatus(status); // Set saved content type final String contentType = resultStore.getContentType(); if (contentType != null) response.setContentType(contentType); // Set saved headers final LinkedHashMap<String, String> headers = resultStore.getHeader(); if (headers != null) for (Map.Entry<String, String> entry : headers.entrySet()) { response.setHeader(entry.getKey(), entry.getValue()); } // Set length since we know it response.setContentLength(resultStore.length(pipelineContext)); } // Replay content resultStore.replay(pipelineContext); } } else { // Local caching is not enabled, just read the input readInput(pipelineContext, response, dataInput, config); httpOutputStream.close(); } } catch (java.net.SocketException e) { // In general there is no point doing much with such exceptions. They are thrown in particular when the // client has closed the connection. logger.info("SocketException in serializer"); } } catch (Exception e) { throw new OXFException(e); } } protected Config readConfig(PipelineContext context) { return readCacheInputAsObject(context, getInputByName(INPUT_CONFIG), new CacheableInputReader<Config>() { public Config read(PipelineContext context, ProcessorInput input) { Element configElement = readInputAsOrbeonDom(context, input).getRootElement(); try { String contentType = XPathUtils.selectStringValueNormalize(configElement, "/config/content-type"); Integer statusCode = XPathUtils.selectIntegerValue(configElement, "/config/status-code"); Integer errorCode = XPathUtils.selectIntegerValue(configElement, "/config/error-code"); String method = XPathUtils.selectStringValueNormalize(configElement, "/config/method"); String version = XPathUtils.selectStringValueNormalize(configElement, "/config/version"); String publicDoctype = XPathUtils.selectStringValueNormalize(configElement, "/config/public-doctype"); String systemDoctype = XPathUtils.selectStringValueNormalize(configElement, "/config/system-doctype"); String encoding = XPathUtils.selectStringValueNormalize(configElement, "/config/encoding"); Integer indentAmount = XPathUtils.selectIntegerValue(configElement, "/config/indent-amount"); Config config = new Config(); // HTTP-specific configuration config.statusCode = statusCode == null ? DEFAULT_STATUS_CODE : statusCode; config.errorCode = errorCode == null ? DEFAULT_ERROR_CODE : errorCode; config.contentType = contentType; config.forceContentType = ProcessorUtils.selectBooleanValue(configElement, "/config/force-content-type", DEFAULT_FORCE_CONTENT_TYPE); if (config.forceContentType && (contentType == null || contentType.equals(""))) throw new OXFException("The force-content-type element requires a content-type element."); config.ignoreDocumentContentType = ProcessorUtils.selectBooleanValue(configElement, "/config/ignore-document-content-type", DEFAULT_IGNORE_DOCUMENT_CONTENT_TYPE); config.encoding = encoding; config.forceEncoding = ProcessorUtils.selectBooleanValue(configElement, "/config/force-encoding", DEFAULT_FORCE_ENCODING); if (config.forceEncoding && (encoding == null || encoding.equals(""))) throw new OXFException("The force-encoding element requires an encoding element."); config.ignoreDocumentEncoding = ProcessorUtils.selectBooleanValue(configElement, "/config/ignore-document-encoding", DEFAULT_IGNORE_DOCUMENT_ENCODING); // Headers for (Iterator i = XPathUtils.selectNodeIterator(configElement, "/config/header"); i.hasNext();) { Element header = (Element) i.next(); String name = header.element("name").getTextTrim(); String value = header.element("value").getTextTrim(); config.addHeader(name, value); } config.headersToForward = XPathUtils.selectStringValue(configElement, "/config/forward-headers"); config.empty = ProcessorUtils.selectBooleanValue(configElement, "/config/empty-content", DEFAULT_EMPTY); // Cache control config.cacheUseLocalCache = ProcessorUtils.selectBooleanValue(configElement, "/config/cache-control/use-local-cache", DEFAULT_CACHE_USE_LOCAL_CACHE); // XML / HTML / Text configuration config.method = method; config.version = version; config.publicDoctype = publicDoctype; config.systemDoctype = systemDoctype; config.omitXMLDeclaration = ProcessorUtils.selectBooleanValue(configElement, "/config/omit-xml-declaration", DEFAULT_OMIT_XML_DECLARATION); String standaloneString = XPathUtils.selectStringValueNormalize(configElement, "/config/standalone"); config.standalone = (standaloneString == null) ? null : Boolean.valueOf(standaloneString); config.indent = ProcessorUtils.selectBooleanValue(configElement, "/config/indent", DEFAULT_INDENT); if (indentAmount != null) config.indentAmount = indentAmount; return config; } catch (Exception e) { throw new OXFException(e); } } }); } /** * Represent the complete serializer configuration. */ public static class Config { // HTTP-specific configuration public int statusCode = DEFAULT_STATUS_CODE; public int errorCode = DEFAULT_ERROR_CODE; public String contentType; public boolean forceContentType = DEFAULT_FORCE_CONTENT_TYPE; public boolean ignoreDocumentContentType = DEFAULT_IGNORE_DOCUMENT_CONTENT_TYPE; public String encoding = DEFAULT_ENCODING; public boolean forceEncoding = DEFAULT_FORCE_ENCODING; public boolean ignoreDocumentEncoding = DEFAULT_IGNORE_DOCUMENT_ENCODING; public List<String> headers; public String headersToForward; public boolean cacheUseLocalCache = DEFAULT_CACHE_USE_LOCAL_CACHE; public boolean empty = DEFAULT_EMPTY; // XML / HTML / Text configuration public String method; public String version; public String publicDoctype; public String systemDoctype; public boolean omitXMLDeclaration = DEFAULT_OMIT_XML_DECLARATION; public Boolean standalone; public boolean indent = DEFAULT_INDENT; public int indentAmount = DEFAULT_INDENT_AMOUNT; public void addHeader(String name, String value) { if (headers == null) headers = new ArrayList<String>(); headers.add(name); headers.add(value); } } /** * Implement the content type determination algorithm. * * @param config current HTTP serializer configuration * @param contentTypeAttribute content type and encoding from the input XML document, or null * @param defaultContentType content type to return if none can be found * @return content type determined */ protected static String getContentType(Config config, String contentTypeAttribute, String defaultContentType) { if (config.forceContentType) return config.contentType; String documentContentType = ContentTypes.getContentTypeMediaTypeOrNull(contentTypeAttribute); if (!config.ignoreDocumentContentType && documentContentType != null) return documentContentType; String userContentType = config.contentType; if (userContentType != null) return userContentType; return defaultContentType; } /** * Implement the encoding determination algorithm. * * @param config current HTTP serializer configuration * @param contentTypeAttribute content type and encoding from the input XML document, or null * @param defaultEncoding encoding to return if none can be found * @return encoding determined */ protected static String getEncoding(Config config, String contentTypeAttribute, String defaultEncoding) { if (config.forceEncoding) return config.encoding; String documentEncoding = ContentTypes.getContentTypeCharsetOrNull(contentTypeAttribute); if (!config.ignoreDocumentEncoding && documentEncoding != null) return documentEncoding; String userEncoding = config.encoding; if (userEncoding != null) return userEncoding; return defaultEncoding; } /** * ResultStoreOutputStream with additional content-type storing. */ private static class ExtendedResultStoreOutputStream extends ResultStoreOutputStream { private String contentType; private int status; private LinkedHashMap<String, String> headers; public ExtendedResultStoreOutputStream(OutputStream out) { super(out); } public void setContentType(String contentType) { this.contentType = contentType; } public void setStatus(int status) { this.status = status; } public void setHeader(String name, String value) { if (headers == null) headers = new LinkedHashMap<String, String>(); headers.put(name, value); } public String getContentType() { return contentType; } public int getStatus() { return status; } public LinkedHashMap<String, String> getHeader() { return headers; } } }