com.day.durbo.DurboOutput Maven / Gradle / Ivy
/*
* Copyright 1997-2008 Day Management AG
* Barfuesserplatz 6, 4001 Basel, Switzerland
* All Rights Reserved.
*
* This software is the confidential and proprietary information of
* Day Management AG, ("Confidential Information"). You shall not
* disclose such Confidential Information and shall use it only in
* accordance with the terms of the license agreement you entered into
* with Day.
*/
package com.day.durbo;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import javax.jcr.NamespaceException;
import javax.jcr.Property;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import org.apache.commons.io.IOUtils;
import com.day.durbo.impl.DurboOutputStream;
import com.day.durbo.io.ChunkedDeflaterOutputStream;
import com.day.durbo.io.ChunkedInflaterInputStream;
/**
* The DurboOutput class provides base implementation for the Durbo
* serialization. The Durbo is hierarchical name-value pair serialization
* that is optimized for stream based operation. the basic principal is, that
* every data element is stored using RLE encoding, i.e. consists of the size
* of the data, followed by the data itself. on the logical level, those elements
* can be properties or nodes. the properties consist of zero, one or more
* values, which are either strings or binaries. the properties itself also
* hold a high-level property type, namely the {@link PropertyType}s from jsr170.
* the additional (invisible) element 'namespace' is used to sequentially define
* namespaces that will be used in the subsequent elements.
*
* since version 2.1 the deflater of the output stream is reset about every 2^12
* bytes (1mb). this generates a output stream that consists of several
* compressed chunks. this means that a possible reader must be prepared for
* resetting it's inflater as well. see {@link ChunkedDeflaterOutputStream} and
* {@link ChunkedInflaterInputStream} for details. this has only an advantage
* during deserialization when large binary properties are kept in
* {@link DurboValue}s and needed to be read from the consumer.
*
* Version 2.0 introduced:
*
* - JCR property type support
*
- namespace support
*
- zip compression
*
*
* Version 2.1 introduced:
*
* - chunked zip compression
*
*
*
*
* durbo:= header {elem};
* header:= hdrVersion hdrContentType hdrEncoding;
* hdrVersion := svProp; // with name {@link DurboConstants#PROTOCOL_HEADER}
* hdrContentType := svProp; // with name {@link DurboConstants#PROTOCOL_CONTENT_TYPE}
* hdrEncoding := svProp; // with name {@link DurboConstants#PROTOCOL_ENCODING}
*
* elem:= node | prop | namespace;
* node:= NODE_START name elemlist NODE_END;
* prop:= svProp | mvProp;
* svProp:= PROPERTY|type name value;
* mvProp:= PROPERTY|MULTIPLE|type name <# values> {value};
* namespace:= NAMESPACE prefix uri;
* prefix:= sValue;
* uri := sValue;
* name:= sValue;
* value:= sValue | bValue;
*
* sValue:= ;
* bValue:= {byte};
*
* NODE_START:= {@link DurboConstants#NODE_START};
* NODE_END:= {@link DurboConstants#NODE_END};
* PROPERTY:= {@link DurboConstants#PROPERTY};
* MULTIPLE:= {@link DurboConstants#MULTIPLE};
* NAMESPACE:= {@link DurboConstants#NAMESPACE};
*
*
*/
public class DurboOutput implements DurboConstants {
/**
* version 1.0 binary property type
*/
private static final int PROPERTY_TYPE_BINARY_V1 = 0x10;
/**
* version 1.0 string property type
*/
private static final int PROPERTY_TYPE_STRING_V1 = 0x11;
/**
* output stream
*/
private final DurboOutputStream out;
/**
* the namespace resolver
*/
private final DurboNamespaceResolver resolver;
/**
* defined namespaces
*/
private final Map namespaces = new HashMap();
/**
* the version to use
*/
private final double version;
/**
* Creates a new DurboOutput
that uses the given output stream.
* this also writes the protocol header.
*
* please note that the protocol version is set to {@link DurboConstants#PROTOCOL_VERSION_1}
* which is probably what you want.
*
* please note that this uses the {@link IdentityNamespaceResolver} and can
* therefor generate weird results upon deserialization.
*
* @param out the output stream
* @throws IOException if an error occurs
*/
public DurboOutput(OutputStream out) throws IOException {
this(out, new IdentityNamespaceResolver(), null, false, PROTOCOL_VERSION_1);
}
/**
* Creates a new DurboOutput
that uses the given output stream.
* this also writes the protocol header.
*
* please note that this uses the {@link IdentityNamespaceResolver} and can
* therefor generate weird results upon deserialization.
*
* @param out the output stream
* @param version the protocol version to use. default is
* {@link DurboConstants#PROTOCOL_VERSION}
* @throws IOException if an error occurs
*/
public DurboOutput(OutputStream out, double version) throws IOException {
this(out, new IdentityNamespaceResolver(), null, false, version);
}
/**
* Creates a new DurboOutput
that uses the given output stream.
* this also writes the protocol header.
*
* @param out the output stream
* @param resolver the namespace resolver
* @throws IOException if an error occurs
*/
public DurboOutput(OutputStream out, DurboNamespaceResolver resolver) throws IOException {
this(out, resolver, null, false);
}
/**
* Creates a new DurboOutput
that uses the given output stream.
* this also writes the protocol header.
*
* @param out the output stream
* @param resolver the namespace resolver
* @param version the protocol version to use. default is
* {@link DurboConstants#PROTOCOL_VERSION}
* @throws IOException if an error occurs
*/
public DurboOutput(OutputStream out, DurboNamespaceResolver resolver, double version) throws IOException {
this(out, resolver, null, false, version);
}
/**
* Creates a new DurboOutput
that uses the given output stream.
* this also writes the protocol header.
*
* @param out the output stream
* @param resolver the namespace resolver
* @param contentType the content type to include in the header. default is
* {@link DurboConstants#DEFAULT_CONTENT_TYPE}
* @param compressed if true
output stream will be compressed.
* @throws IOException if an error occurs
*/
public DurboOutput(OutputStream out, DurboNamespaceResolver resolver, String contentType, boolean compressed)
throws IOException {
this(out, resolver, contentType, compressed, PROTOCOL_VERSION);
}
/**
* Creates a new DurboOutput
that uses the given output stream.
* this also writes the protocol header.
*
* @param out the output stream
* @param resolver the namespace resolver
* @param contentType the content type to include in the header. default is
* {@link DurboConstants#DEFAULT_CONTENT_TYPE}
* @param compressed if true
output stream will be compressed.
* @param version the protocol version to use. default is
* {@link DurboConstants#PROTOCOL_VERSION}
* @throws IOException if an error occurs
*/
public DurboOutput(OutputStream out, DurboNamespaceResolver resolver, String contentType, boolean compressed, double version)
throws IOException {
this.resolver = resolver;
this.version = version;
this.out = new DurboOutputStream(out);
if (compressed && version < PROTOCOL_VERSION_2) {
throw new UnsupportedOperationException("Compression is not supported in protocol version " + version);
}
if (contentType != null && version < PROTOCOL_VERSION_2) {
throw new UnsupportedOperationException("ContentType is not supported in protocol version " + version);
}
// write header
writeProperty(PROTOCOL_HEADER, String.valueOf(version));
if (version >= PROTOCOL_VERSION_2) {
writeProperty(PROTOCOL_CONTENT_TYPE, contentType == null ? DEFAULT_CONTENT_TYPE : contentType);
writeProperty(PROTOCOL_ENCODING, compressed ? "zip" : "");
}
if (compressed) {
this.out.enableCompression();
}
}
/**
* closes the output.
*
* @throws IOException if an I/O error occurs
*/
public void close() throws IOException {
this.out.close();
}
/**
* writes a namespace definition to the stream.
*
* @param prefix the namespace prefix
* @param uri the namespace uri
* @throws IOException if an I/O exception occurs
*/
public void defineNamespace(String prefix, String uri) throws IOException {
if (version < PROTOCOL_VERSION_2) {
throw new UnsupportedOperationException("Namespaces are not supported in protocol version " + version);
}
if (namespaces.containsKey(prefix)) {
// check if correct uri
if (!uri.equals(namespaces.get(prefix))) {
throw new UnsupportedOperationException("Namespace remapping not implemented.");
}
} else {
out.writeByte(NAMESPACE);
out.write(prefix);
out.write(uri);
namespaces.put(prefix, uri);
}
}
/**
* Writes a binary property with the given name
and data
to the output.
*
* @param name the name of the property
* @param data the data to write to the stream
* @throws IOException if an error occurs
*/
public void writeProperty(String name, byte[] data) throws IOException {
writeHeader(PROPERTY | PropertyType.BINARY, name);
out.write(data);
}
/**
* Writes a string property with the given name
and data
* to the output.
*
* @param name the name of the property
* @param data the data to write to the stream
* @throws IOException if an error occurs
*/
public void writeProperty(String name, String data) throws IOException {
writeHeader(PROPERTY | PropertyType.STRING, name);
out.write(data);
}
/**
* Writes a JCR property to the output
*
* @param prop the property to write
* @throws IOException if an I/O error occurs
* @throws RepositoryException if an error occurs
*/
public void writeProperty(Property prop) throws IOException, RepositoryException {
Value[] values = prop.getDefinition().isMultiple() ? prop.getValues() : new Value[]{prop.getValue()};
if (version >= PROTOCOL_VERSION_2) {
// check for namespaces in properties
if (prop.getType() == PropertyType.NAME) {
for (Value value : values) {
checkNamespace(value.getString());
}
} else if (prop.getType() == PropertyType.PATH) {
for (Value value : values) {
checkNamespacesInPath(value.getString());
}
}
}
if (prop.getDefinition().isMultiple()) {
if (version < PROTOCOL_VERSION_2) {
throw new UnsupportedOperationException("Multivalue properties are not supported in protocol version " + version);
}
writeHeader(PROPERTY | MULTIPLE | prop.getType(), prop.getName());
out.writeInt(values.length);
for (Value value : values) {
out.write(value);
}
} else {
writeHeader(PROPERTY | prop.getType(), prop.getName());
out.write(values[0]);
}
}
/**
* Writes the durbo property to the output
*
* @param prop the property
* @throws IOException if an I/O error occurs
*/
public void writeProperty(DurboInput.Property prop) throws IOException {
DurboValue[] values = prop.getValues();
if (prop.isMultiple()) {
if (version < PROTOCOL_VERSION_2) {
throw new UnsupportedOperationException("Multivalue properties are not supported in protocol version " + version);
}
writeHeader(PROPERTY | MULTIPLE | prop.getType(), prop.name());
out.writeInt(prop.getValues().length);
for (DurboValue value : values) {
out.write(value);
}
} else {
writeHeader(PROPERTY | prop.getType(), prop.name());
out.write(values[0]);
}
}
/**
* Writes a typed multivalue property.
*
* @param name name of the property
* @param type type of the property
* @param values values of the property
* @throws IOException if an I/O error occurs
*/
public void writeProperty(String name, int type, String[] values) throws IOException {
writeProperty(name, type, values, true);
}
/**
* Writes a typed single value property.
*
* @param name name of the property
* @param type type of the property
* @param value value of the property
* @throws IOException if an I/O error occurs
*/
public void writeProperty(String name, int type, String value) throws IOException {
writeProperty(name, type, new String[]{value}, false);
}
/**
* Writes a typed property.
*
* @param name name of the property
* @param type type of the property
* @param values values of the property
* @param isMultiple true
if a mv property
* @throws IOException if an I/O error occurs
*/
private void writeProperty(String name, int type, String[] values, boolean isMultiple) throws IOException {
if (version >= PROTOCOL_VERSION_2) {
// check for namespaces in properties
if (type == PropertyType.NAME) {
for (String value : values) {
checkNamespace(value);
}
} else if (type == PropertyType.PATH) {
for (String value : values) {
checkNamespacesInPath(value);
}
}
}
if (isMultiple) {
if (version < PROTOCOL_VERSION_2) {
throw new UnsupportedOperationException("Multivalue properties are not supported in protocol version " + version);
}
writeHeader(PROPERTY | MULTIPLE | type, name);
out.writeInt(values.length);
for (String value : values) {
out.write(value);
}
} else {
writeHeader(PROPERTY | type, name);
out.write(values[0]);
}
}
/**
* Checks the namespaces in all the path elements.
*
* @param path path to check
* @throws IOException if an I/O error occurs
* @see #checkNamespace(String)
*/
private void checkNamespacesInPath(String path) throws IOException {
// check path elements
int pos, lastpos = 0;
while ((pos = path.indexOf('/', lastpos)) >= 0) {
if (pos - lastpos > 0)
checkNamespace(path.substring(lastpos, pos));
lastpos = pos + 1;
}
// check rest
if (lastpos < path.length()) {
checkNamespace(path.substring(lastpos));
}
}
/**
* Write a binary property with the given name
and data provided in
* the input stream to to the output.
*
* @param name the name of the property
* @param in the data to write to the stream
* @throws IOException if an error occurs
*/
public void writeProperty(String name, InputStream in) throws IOException {
// TODO: use buffered write below?
ByteArrayOutputStream tmp = new ByteArrayOutputStream();
IOUtils.copy(in, tmp);
in.close();
writeProperty(name, tmp.toByteArray());
}
/**
* Write a property with the given name
and data provided in
* the input stream to to the output.
*
* @param name the name of the property
* @param in the data to write to the stream
* @param size The number of bytes to write from the InputStream
.
* If this value is negative, as much is read from the input stream
* as possible.
* @throws IOException if an error occurs
*/
public void writeProperty(String name, InputStream in, int size) throws IOException {
// TODO: use buffered write below?
writeHeader(PROPERTY | PropertyType.BINARY, name);
if (size < 0) {
out.write(in);
} else {
out.write(in, size);
}
}
/**
* Writes an open node marker to the output with the given node
* name
.
*
* @param name the name of the node
* @throws IOException if an error occurs
*/
public void openNode(String name) throws IOException {
writeHeader(NODE_START, name);
}
/**
* Writes an closing node marker to the output.
*
* @throws java.io.IOException if an error occurs
*/
public void closeNode() throws IOException {
out.writeByte(NODE_END);
}
/**
* writes a header
*
* @param type property type of the header
* @param name name of the header
* @throws IOException if an I/O error occurs
*/
private void writeHeader(int type, String name) throws IOException {
if (version >= PROTOCOL_VERSION_2) {
checkNamespace(name);
} else {
if ((type & PROPERTY) > 0) {
if ((type & PROPERTY_TYPE_MASK) == PropertyType.BINARY) {
type = PROPERTY_TYPE_BINARY_V1;
} else {
type = PROPERTY_TYPE_STRING_V1;
}
}
}
out.writeByte(type);
out.write(name);
}
/**
* checks if the namespace pointed to by the prefix of the name exists
* and is already defined in the output.
*
* @param name name of the namespace
* @throws IOException if an I/O error occurs
*/
private void checkNamespace(String name) throws IOException {
int pos = name.indexOf(':');
if (pos > 0) {
// check namespace
String prefix = name.substring(0, pos);
try {
defineNamespace(prefix, resolver.getURI(prefix));
} catch (NamespaceException e) {
throw new IllegalArgumentException(e.toString());
}
}
}
}