/**
* Copyright (C) 2012 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.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.apache.log4j.Logger;
import org.orbeon.dom.Document;
import org.orbeon.oxf.common.OXFException;
import org.orbeon.oxf.pipeline.api.PipelineContext;
import org.orbeon.oxf.processor.*;
import org.orbeon.oxf.processor.serializer.store.ResultStore;
import org.orbeon.oxf.processor.serializer.store.ResultStoreOutputStream;
import org.orbeon.oxf.util.LoggerFactory;
import org.orbeon.oxf.util.NetUtils;
import org.orbeon.oxf.xforms.processor.XFormsResourceServer;
import org.orbeon.oxf.xml.SAXUtils;
import org.orbeon.oxf.xml.XMLReceiver;
import org.orbeon.oxf.xml.XPathUtils;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* The File Serializer serializes text and binary documents to files on disk.
*/
public class FileSerializer extends ProcessorImpl {
private static Logger logger = LoggerFactory.createLogger(FileSerializer.class);
public static final String FILE_SERIALIZER_CONFIG_NAMESPACE_URI = "http://orbeon.org/oxf/xml/file-serializer-config";
public static final String DIRECTORY_PROPERTY = "directory";
// NOTE: Those are also in HttpSerializerBase
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 final boolean DEFAULT_APPEND = false;
private static final boolean DEFAULT_MAKE_DIRECTORIES = false;
static {
try {
// Create factory
DocumentBuilderFactory documentBuilderFactory = (DocumentBuilderFactory) Class.forName("orbeon.apache.xerces.jaxp.DocumentBuilderFactoryImpl").newInstance();
// Configure factory
documentBuilderFactory.setNamespaceAware(true);
}
catch (Exception e) {
throw new OXFException(e);
}
}
public FileSerializer() {
addInputInfo(new ProcessorInputOutputInfo(INPUT_CONFIG, FILE_SERIALIZER_CONFIG_NAMESPACE_URI));
addInputInfo(new ProcessorInputOutputInfo(INPUT_DATA));
// We don't declare the "data" output here, as this is an optional output.
// If we declare it, we'll the XPL engine won't be happy when don't connect anything to that output.
}
private static class Config {
private String directory;
private String file;
private String scope;
private boolean proxyResult;
private String url;
private boolean append;
private boolean makeDirectories;
private boolean cacheUseLocalCache;
private boolean forceContentType;
private String requestedContentType;
private boolean ignoreDocumentContentType;
private boolean forceEncoding;
private String requestedEncoding;
private boolean ignoreDocumentEncoding;
public Config(Document document) {
// Directory and file
directory = XPathUtils.selectStringValueNormalize(document, "/config/directory");
file = XPathUtils.selectStringValueNormalize(document, "/config/file");
// Scope
scope = XPathUtils.selectStringValueNormalize(document, "/config/scope");
// Proxy result
proxyResult = ProcessorUtils.selectBooleanValue(document, "/config/proxy-result", false);
// URL
url = XPathUtils.selectStringValueNormalize(document, "/config/url");
// Cache control
cacheUseLocalCache = ProcessorUtils.selectBooleanValue(document, "/config/cache-control/use-local-cache", CachedSerializer.DEFAULT_CACHE_USE_LOCAL_CACHE);
// Whether to append or not
append = ProcessorUtils.selectBooleanValue(document, "/config/append", DEFAULT_APPEND);
// Whether to append or not
makeDirectories = ProcessorUtils.selectBooleanValue(document, "/config/make-directories", DEFAULT_MAKE_DIRECTORIES);
// Content-type and Encoding
requestedContentType = XPathUtils.selectStringValueNormalize(document, "/config/content-type");
forceContentType = ProcessorUtils.selectBooleanValue(document, "/config/force-content-type", DEFAULT_FORCE_CONTENT_TYPE);
// TODO: We don't seem to be using the content type in the file serializer.
// Maybe this is something that was left over from the days when the file serializer was also serializing XML.
if (forceContentType)
throw new OXFException("The force-content-type element requires a content-type element.");
ignoreDocumentContentType = ProcessorUtils.selectBooleanValue(document, "/config/ignore-document-content-type", DEFAULT_IGNORE_DOCUMENT_CONTENT_TYPE);
requestedEncoding = XPathUtils.selectStringValueNormalize(document, "/config/encoding");
forceEncoding = ProcessorUtils.selectBooleanValue(document, "/config/force-encoding", DEFAULT_FORCE_ENCODING);
if (forceEncoding && (requestedEncoding == null || requestedEncoding.equals("")))
throw new OXFException("The force-encoding element requires an encoding element.");
ignoreDocumentEncoding = ProcessorUtils.selectBooleanValue(document, "/config/ignore-document-encoding", DEFAULT_IGNORE_DOCUMENT_ENCODING);
}
public String getDirectory() {
return directory;
}
public String getFile() {
return file;
}
public String getScope() {
return scope;
}
public boolean isProxyResult() {
return proxyResult;
}
public String getUrl() {
return url;
}
public boolean isAppend() {
return append;
}
public boolean isMakeDirectories() {
return makeDirectories;
}
public boolean isCacheUseLocalCache() {
return cacheUseLocalCache;
}
public boolean isForceContentType() {
return forceContentType;
}
public boolean isForceEncoding() {
return forceEncoding;
}
public boolean isIgnoreDocumentContentType() {
return ignoreDocumentContentType;
}
public boolean isIgnoreDocumentEncoding() {
return ignoreDocumentEncoding;
}
public String getRequestedContentType() {
return requestedContentType;
}
public String getRequestedEncoding() {
return requestedEncoding;
}
}
@Override
public void start(PipelineContext context) {
try {
// Read config
final Config config = readCacheInputAsObject(context, getInputByName(INPUT_CONFIG), new CacheableInputReader<Config>() {
public Config read(PipelineContext context, ProcessorInput input) {
return new Config(readInputAsOrbeonDom(context, input));
}
});
final ProcessorInput dataInput = getInputByName(INPUT_DATA);
// Get file object
final String directory = config.getDirectory() != null ? config.getDirectory() : getPropertySet().getString(DIRECTORY_PROPERTY);
final File file = NetUtils.getFile(directory, config.getFile(), config.getUrl(), getLocationData(), config.isMakeDirectories());
// NOTE: Caching here is broken, so we never cache. This is what we should do in case
// we want caching:
// - for a given file, store a hash of the content stored (or the input key?)
// - then when we check whether we need to modify the file, check against the key
// AND the validity
// Delete file if it exists, unless we append
if (!config.isAppend() && file.exists()) {
final boolean deleted = file.delete();
// We test on file.exists() here again so we don't complain that the file can't be deleted if it got
// deleted just between our last test and the delete operation.
if (!deleted && file.exists())
throw new OXFException("Can't delete file: " + file);
}
// Create file if needed
file.createNewFile();
FileOutputStream fileOutputStream = new FileOutputStream(file, config.isAppend());
writeToFile(context, config, dataInput, fileOutputStream);
} catch (Exception e) {
throw new OXFException(e);
}
}
private void writeToFile(PipelineContext context, final Config config, ProcessorInput dataInput, final OutputStream fileOutputStream) throws IOException {
try {
if (config.cacheUseLocalCache) {
// If caching of the data is enabled, use the caching API
// We return a ResultStore
final boolean[] read = new boolean[1];
ResultStore filter = (ResultStore) readCacheInputAsObject(context, dataInput, new CacheableInputReader() {
public Object read(PipelineContext context, ProcessorInput input) {
read[0] = true;
if (logger.isDebugEnabled())
logger.debug("Output not cached");
try {
ResultStoreOutputStream resultStoreOutputStream = new ResultStoreOutputStream(fileOutputStream);
readInputAsSAX(context, input, new BinaryTextXMLReceiver(null, resultStoreOutputStream, true,
config.forceContentType, config.requestedContentType, config.ignoreDocumentContentType,
config.forceEncoding, config.requestedEncoding, config.ignoreDocumentEncoding, null));
resultStoreOutputStream.close();
return resultStoreOutputStream;
} catch (IOException e) {
throw new OXFException(e);
}
}
});
// If the output was obtained from the cache, just write it
if (!read[0]) {
if (logger.isDebugEnabled())
logger.debug("Serializer output cached");
filter.replay(fileOutputStream);
}
} else {
// Caching is not enabled
readInputAsSAX(context, dataInput, new BinaryTextXMLReceiver(null, fileOutputStream, true,
config.forceContentType, config.requestedContentType, config.ignoreDocumentContentType,
config.forceEncoding, config.requestedEncoding, config.ignoreDocumentEncoding, null));
fileOutputStream.close();
}
} finally {
if (fileOutputStream != null)
fileOutputStream.close();
}
}
/**
* Case where a response must be generated.
*/
@Override
public ProcessorOutput createOutput(String name) {
final ProcessorOutput output = new ProcessorOutputImpl(FileSerializer.this, name) {
public void readImpl(PipelineContext pipelineContext, XMLReceiver xmlReceiver) {
OutputStream fileOutputStream = null;
try {
//Get the input and config
final Config config = getConfig(pipelineContext);
final ProcessorInput dataInput = getInputByName(INPUT_DATA);
// Determine scope
final int scope;
if ("request".equals(config.getScope())) {
scope = NetUtils.REQUEST_SCOPE;
} else if ("session".equals(config.getScope())) {
scope = NetUtils.SESSION_SCOPE;
} else if ("application".equals(config.getScope())) {
scope = NetUtils.APPLICATION_SCOPE;
} else {
throw new OXFException("Invalid context requested: " + config.getScope());
}
// We use the commons fileupload utilities to write to file
final FileItem fileItem = NetUtils.prepareFileItem(scope, logger);
fileOutputStream = fileItem.getOutputStream();
writeToFile(pipelineContext, config, dataInput, fileOutputStream);
// Create file if it doesn't exist
final File storeLocation = ((DiskFileItem) fileItem).getStoreLocation();
storeLocation.createNewFile();
// Get the url of the file
final String resultURL;
{
final String localURL = ((DiskFileItem) fileItem).getStoreLocation().toURI().toString();
if ("session".equals(config.getScope()) && config.isProxyResult())
resultURL = XFormsResourceServer.jProxyURI(localURL, config.getRequestedContentType());
else
resultURL = localURL;
}
xmlReceiver.startDocument();
xmlReceiver.startElement("", "url", "url", SAXUtils.EMPTY_ATTRIBUTES);
xmlReceiver.characters(resultURL.toCharArray(), 0, resultURL.length());
xmlReceiver.endElement("", "url", "url");
xmlReceiver.endDocument();
}
catch (SAXException e) {
throw new OXFException(e);
}
catch (IOException e) {
throw new OXFException(e);
}
finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
}
catch (IOException e) {
throw new OXFException(e);
}
}
}
}
};
addOutput(name, output);
return output;
}
protected Config getConfig(PipelineContext pipelineContext) {
// Read config
return readCacheInputAsObject(pipelineContext, getInputByName(INPUT_CONFIG), new CacheableInputReader<Config>() {
public Config read(PipelineContext context, ProcessorInput input) {
return new Config(readInputAsOrbeonDom(context, input));
}
});
}
}