/*
* 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.jackrabbit.core.persistence.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.commons.io.IOExceptionWithCause;
import org.apache.commons.io.input.CountingInputStream;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.id.PropertyId;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Calendar;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.Set;
import java.util.TimeZone;
import java.math.BigDecimal;
import javax.jcr.PropertyType;
/**
* Bundle deserializer. See the {@link BundleWriter} class for details of
* the serialization format.
*
* @see BundleWriter
*/
class BundleReader {
/*
* Implementation note: if you change this class, also change BundleDumper
* accordingly.
*/
/** Logger instance */
private static Logger log = LoggerFactory.getLogger(BundleReader.class);
/**
* Pre-calculated {@link TimeZone} objects for common timezone offsets.
*/
private static final TimeZone[] COMMON_TIMEZONES = {
TimeZone.getTimeZone("GMT+00:00"), // 0b00000
TimeZone.getTimeZone("GMT+01:00"), // 0b00001
TimeZone.getTimeZone("GMT+02:00"), // 0b00010
TimeZone.getTimeZone("GMT+03:00"), // 0b00011
TimeZone.getTimeZone("GMT+04:00"), // 0b00100
TimeZone.getTimeZone("GMT+05:00"), // 0b00101
TimeZone.getTimeZone("GMT+06:00"), // 0b00110
TimeZone.getTimeZone("GMT+07:00"), // 0b00111
TimeZone.getTimeZone("GMT+08:00"), // 0b01000
TimeZone.getTimeZone("GMT+09:00"), // 0b01001
TimeZone.getTimeZone("GMT+10:00"), // 0b01010
TimeZone.getTimeZone("GMT+11:00"), // 0b01011
TimeZone.getTimeZone("GMT+12:00"), // 0b01100
TimeZone.getTimeZone("GMT+13:00"), // 0b01101
TimeZone.getTimeZone("GMT+14:00"), // 0b01110
TimeZone.getTimeZone("GMT+15:00"), // 0b01111
TimeZone.getTimeZone("GMT-16:00"), // 0b10000
TimeZone.getTimeZone("GMT-15:00"), // 0b10001
TimeZone.getTimeZone("GMT-14:00"), // 0b10010
TimeZone.getTimeZone("GMT-13:00"), // 0b10011
TimeZone.getTimeZone("GMT-12:00"), // 0b10100
TimeZone.getTimeZone("GMT-11:00"), // 0b10101
TimeZone.getTimeZone("GMT-10:00"), // 0b10110
TimeZone.getTimeZone("GMT-09:00"), // 0b10111
TimeZone.getTimeZone("GMT-08:00"), // 0b11000
TimeZone.getTimeZone("GMT-07:00"), // 0b11001
TimeZone.getTimeZone("GMT-06:00"), // 0b11010
TimeZone.getTimeZone("GMT-05:00"), // 0b11011
TimeZone.getTimeZone("GMT-04:00"), // 0b11100
TimeZone.getTimeZone("GMT-03:00"), // 0b11101
TimeZone.getTimeZone("GMT-02:00"), // 0b11110
TimeZone.getTimeZone("GMT-01:00"), // 0b11111
};
private final BundleBinding binding;
/**
* Counter for the number of bytes read from the input stream.
*/
private final CountingInputStream cin;
/**
* Wrapper for reading structured data from the input stream.
*/
private final DataInputStream in;
private final int version;
/**
* The default namespace and the first six other namespaces used in this
* bundle. Used by the {@link #readName()} method to keep track of
* already seen namespaces.
*/
private final String[] namespaces =
// NOTE: The length of this array must be seven
{ Name.NS_DEFAULT_URI, null, null, null, null, null, null };
/**
* Creates a new bundle deserializer.
*
* @param binding bundle binding
* @param stream stream from which the bundle is read
* @throws IOException if an I/O error occurs.
*/
public BundleReader(BundleBinding binding, InputStream stream)
throws IOException {
this.binding = binding;
this.cin = new CountingInputStream(stream);
this.in = new DataInputStream(cin);
this.version = in.readUnsignedByte();
}
/**
* Deserializes a <code>NodePropBundle</code> from a data input stream.
*
* @param id the node id for the new bundle
* @return the bundle
* @throws IOException if an I/O error occurs.
*/
public NodePropBundle readBundle(NodeId id) throws IOException {
long start = cin.getByteCount();
NodePropBundle bundle = new NodePropBundle(id);
if (version >= BundleBinding.VERSION_3) {
readBundleNew(bundle);
} else {
readBundleOld(bundle);
}
bundle.setSize(cin.getByteCount() - start);
return bundle;
}
private void readBundleNew(NodePropBundle bundle) throws IOException {
// node type
bundle.setNodeTypeName(readName());
// parentUUID
NodeId parentId = readNodeId();
if (BundleBinding.NULL_PARENT_ID.equals(parentId)) {
parentId = null;
}
bundle.setParentId(parentId);
// read modcount
bundle.setModCount((short) readVarInt());
int b = in.readUnsignedByte();
bundle.setReferenceable((b & 1) != 0);
// mixin types
int mn = readVarInt((b >> 7) & 1, 1);
if (mn == 0) {
bundle.setMixinTypeNames(Collections.<Name>emptySet());
} else if (mn == 1) {
bundle.setMixinTypeNames(Collections.singleton(readName()));
} else {
Set<Name> mixins = new HashSet<Name>(mn * 2);
for (int i = 0; i < mn; i++) {
mixins.add(readName());
}
bundle.setMixinTypeNames(mixins);
}
// properties
int pn = readVarInt((b >> 4) & 7, 7);
for (int i = 0; i < pn; i++) {
PropertyId id = new PropertyId(bundle.getId(), readName());
bundle.addProperty(readPropertyEntry(id));
}
// child nodes (list of name/uuid pairs)
int nn = readVarInt((b >> 2) & 3, 3);
for (int i = 0; i < nn; i++) {
Name name = readQName();
NodeId id = readNodeId();
bundle.addChildNodeEntry(name, id);
}
// read shared set
int sn = readVarInt((b >> 1) & 1, 1);
if (sn == 0) {
bundle.setSharedSet(Collections.<NodeId>emptySet());
} else if (sn == 1) {
bundle.setSharedSet(Collections.singleton(readNodeId()));
} else {
Set<NodeId> shared = new HashSet<NodeId>();
for (int i = 0; i < sn; i++) {
shared.add(readNodeId());
}
bundle.setSharedSet(shared);
}
}
private void readBundleOld(NodePropBundle bundle) throws IOException {
// read primary type...special handling
int a = in.readUnsignedByte();
int b = in.readUnsignedByte();
int c = in.readUnsignedByte();
String uri = binding.nsIndex.indexToString(a << 16 | b << 8 | c);
String local = binding.nameIndex.indexToString(in.readInt());
bundle.setNodeTypeName(
NameFactoryImpl.getInstance().create(uri, local));
// parentUUID
bundle.setParentId(readNodeId());
// definitionId
in.readUTF();
// mixin types
Name name = readIndexedQName();
if (name != null) {
Set<Name> mixinTypeNames = new HashSet<Name>();
do {
mixinTypeNames.add(name);
name = readIndexedQName();
} while (name != null);
bundle.setMixinTypeNames(mixinTypeNames);
} else {
bundle.setMixinTypeNames(Collections.<Name>emptySet());
}
// properties
name = readIndexedQName();
while (name != null) {
PropertyId pId = new PropertyId(bundle.getId(), name);
NodePropBundle.PropertyEntry pState = readPropertyEntry(pId);
// skip redundant primaryType, mixinTypes and uuid properties
if (!name.equals(NameConstants.JCR_PRIMARYTYPE)
&& !name.equals(NameConstants.JCR_MIXINTYPES)
&& !name.equals(NameConstants.JCR_UUID)) {
bundle.addProperty(pState);
}
name = readIndexedQName();
}
// set referenceable flag
bundle.setReferenceable(in.readBoolean());
// child nodes (list of uuid/name pairs)
NodeId childId = readNodeId();
while (childId != null) {
bundle.addChildNodeEntry(readQName(), childId);
childId = readNodeId();
}
// read modcount, since version 1.0
if (version >= BundleBinding.VERSION_1) {
bundle.setModCount(in.readShort());
}
// read shared set, since version 2.0
if (version >= BundleBinding.VERSION_2) {
// shared set (list of parent uuids)
NodeId parentId = readNodeId();
if (parentId != null) {
Set<NodeId> shared = new HashSet<NodeId>();
do {
shared.add(parentId);
parentId = readNodeId();
} while (parentId != null);
bundle.setSharedSet(shared);
} else {
bundle.setSharedSet(Collections.<NodeId>emptySet());
}
} else {
bundle.setSharedSet(Collections.<NodeId>emptySet());
}
}
/**
* Deserializes a <code>PropertyState</code> from the data input stream.
*
* @param id the property id for the new property entry
* @return the property entry
* @throws IOException if an I/O error occurs.
*/
private NodePropBundle.PropertyEntry readPropertyEntry(PropertyId id)
throws IOException {
NodePropBundle.PropertyEntry entry = new NodePropBundle.PropertyEntry(id);
int count = 1;
if (version >= BundleBinding.VERSION_3) {
int b = in.readUnsignedByte();
entry.setType(b & 0x0f);
int len = b >>> 4;
if (len != 0) {
entry.setMultiValued(true);
if (len == 0x0f) {
count = readVarInt() + 0x0f - 1;
} else {
count = len - 1;
}
}
entry.setModCount((short) readVarInt());
} else {
// type and modcount
int type = in.readInt();
entry.setModCount((short) ((type >> 16) & 0x0ffff));
type &= 0x0ffff;
entry.setType(type);
// multiValued
entry.setMultiValued(in.readBoolean());
// definitionId
in.readUTF();
// count
count = in.readInt();
}
// values
InternalValue[] values = new InternalValue[count];
String[] blobIds = new String[count];
for (int i = 0; i < count; i++) {
InternalValue val;
int type = entry.getType();
switch (type) {
case PropertyType.BINARY:
int size = in.readInt();
if (size == BundleBinding.BINARY_IN_DATA_STORE) {
val = InternalValue.create(binding.dataStore, readString());
} else if (size == BundleBinding.BINARY_IN_BLOB_STORE) {
blobIds[i] = readString();
try {
BLOBStore blobStore = binding.getBlobStore();
if (blobStore instanceof ResourceBasedBLOBStore) {
val = InternalValue.create(((ResourceBasedBLOBStore) blobStore).getResource(blobIds[i]));
} else {
val = InternalValue.create(blobStore.get(blobIds[i]));
}
} catch (IOException e) {
if (binding.errorHandling.ignoreMissingBlobs()) {
log.warn("Ignoring error while reading blob-resource: " + e);
val = InternalValue.create(new byte[0]);
} else {
throw e;
}
} catch (Exception e) {
throw new IOExceptionWithCause("Unable to create property value: " + e.toString(), e);
}
} else {
// short values into memory
byte[] data = new byte[size];
in.readFully(data);
val = InternalValue.create(data);
}
break;
case PropertyType.DOUBLE:
val = InternalValue.create(in.readDouble());
break;
case PropertyType.DECIMAL:
val = InternalValue.create(readDecimal());
break;
case PropertyType.LONG:
if (version >= BundleBinding.VERSION_3) {
val = InternalValue.create(readVarLong());
} else {
val = InternalValue.create(in.readLong());
}
break;
case PropertyType.BOOLEAN:
val = InternalValue.create(in.readBoolean());
break;
case PropertyType.NAME:
val = InternalValue.create(readQName());
break;
case PropertyType.WEAKREFERENCE:
val = InternalValue.create(readNodeId(), true);
break;
case PropertyType.REFERENCE:
val = InternalValue.create(readNodeId(), false);
break;
case PropertyType.DATE:
if (version >= BundleBinding.VERSION_3) {
val = InternalValue.create(readDate());
break;
} // else fall through
default:
if (version >= BundleBinding.VERSION_3) {
val = InternalValue.valueOf(
readString(), entry.getType());
} else {
// because writeUTF(String) has a size limit of 64k,
// Strings are serialized as <length><byte[]>
int len = in.readInt();
byte[] bytes = new byte[len];
in.readFully(bytes);
String stringVal = new String(bytes, "UTF-8");
// https://issues.apache.org/jira/browse/JCR-3083
if (PropertyType.DATE == entry.getType()) {
val = InternalValue.createDate(stringVal);
} else {
val = InternalValue.valueOf(stringVal, entry.getType());
}
}
}
values[i] = val;
}
entry.setValues(values);
entry.setBlobIds(blobIds);
return entry;
}
/**
* Deserializes a node identifier
*
* @return the node id
* @throws IOException in an I/O error occurs.
*/
private NodeId readNodeId() throws IOException {
if (version >= BundleBinding.VERSION_3 || in.readBoolean()) {
long msb = in.readLong();
long lsb = in.readLong();
return new NodeId(msb, lsb);
} else {
return null;
}
}
/**
* Deserializes a BigDecimal
*
* @return the decimal
* @throws IOException in an I/O error occurs.
*/
private BigDecimal readDecimal() throws IOException {
if (in.readBoolean()) {
// TODO more efficient serialization format
return new BigDecimal(readString());
} else {
return null;
}
}
/**
* Deserializes a Name
*
* @return the qname
* @throws IOException in an I/O error occurs.
*/
private Name readQName() throws IOException {
if (version >= BundleBinding.VERSION_3) {
return readName();
}
String uri = binding.nsIndex.indexToString(in.readInt());
String local = in.readUTF();
return NameFactoryImpl.getInstance().create(uri, local);
}
/**
* Deserializes an indexed Name
*
* @return the qname
* @throws IOException in an I/O error occurs.
*/
private Name readIndexedQName() throws IOException {
if (version >= BundleBinding.VERSION_3) {
return readName();
}
int index = in.readInt();
if (index < 0) {
return null;
} else {
String uri = binding.nsIndex.indexToString(index);
String local = binding.nameIndex.indexToString(in.readInt());
return NameFactoryImpl.getInstance().create(uri, local);
}
}
/**
* Deserializes a name written using bundle serialization version 3.
*
* @return deserialized name
* @throws IOException if an I/O error occurs
*/
private Name readName() throws IOException {
int b = in.readUnsignedByte();
if ((b & 0x80) == 0) {
return BundleNames.indexToName(b);
} else {
String uri;
int ns = (b >> 4) & 0x07;
if (ns < namespaces.length && namespaces[ns] != null) {
uri = namespaces[ns];
} else {
uri = readString();
if (ns < namespaces.length) {
namespaces[ns] = uri;
}
}
String local = new String(readBytes((b & 0x0f) + 1, 0x10), "UTF-8");
return NameFactoryImpl.getInstance().create(uri, local);
}
}
/**
* Deserializes a variable-length integer written using bundle
* serialization version 3.
*
* @return deserialized integer
* @throws IOException if an I/O error occurs
*/
private int readVarInt() throws IOException {
int b = in.readUnsignedByte();
if ((b & 0x80) == 0) {
return b;
} else {
return readVarInt() << 7 | b & 0x7f;
}
}
private int readVarInt(int value, int base) throws IOException {
if (value < base) {
return value;
} else {
return readVarInt() + base;
}
}
/**
* Deserializes a variable-length long written using bundle
* serialization version 3.
*
* @return deserialized long
* @throws IOException if an I/O error occurs
*/
private long readVarLong() throws IOException {
long value = 0;
int bits = 0;
long b;
do {
b = in.readUnsignedByte();
if (bits < 57) {
value = (b & 0x7f) << 57 | value >>> 7;
bits += 7;
} else {
value = (b & 0x01) << 63 | value >>> 1;
bits = 64;
}
} while ((b & 0x80) != 0);
value = value >>> (64 - bits);
if ((value & 1) != 0) {
return ~(value >>> 1);
} else {
return value >>> 1;
}
}
/**
* Deserializes a specially encoded date written using bundle
* serialization version 3.
*
* @return deserialized date
* @throws IOException if an I/O error occurs
*/
private Calendar readDate() throws IOException {
long ts = readVarLong();
TimeZone tz;
if ((ts & 1) == 0) {
tz = COMMON_TIMEZONES[0];
ts >>= 1;
} else if ((ts & 2) == 0) {
tz = COMMON_TIMEZONES[((int) ts >> 2) & 0x1f]; // 5 bits;
ts >>= 7;
} else {
int m = ((int) ts << 19) >> 21; // 11 bits, sign-extended
int h = m / 60;
String s;
if (m < 0) {
s = String.format("GMT-%02d:%02d", -h, h * 60 - m);
} else {
s = String.format("GMT+%02d:%02d", h, m - h * 60);
}
tz = TimeZone.getTimeZone(s);
ts >>= 13;
}
int u = 0;
int s = 0;
int m = 0;
int h = 0;
int type = (int) ts & 3;
ts >>= 2;
switch (type) {
case 3:
u = (int) ts & 0x3fffffff; // 30 bits
s = u / 1000;
m = s / 60;
h = m / 60;
m -= h * 60;
s -= (h * 60 + m) * 60;
u -= ((h * 60 + m) * 60 + s) * 1000;
ts >>= 30;
break;
case 2:
m = (int) ts & 0x07ff; // 11 bits
h = m / 60;
m -= h * 60;
ts >>= 11;
break;
case 1:
h = (int) ts & 0x1f; // 5 bits
ts >>= 5;
break;
}
int d = (int) ts & 0x01ff; // 9 bits;
ts >>= 9;
int y = (int) (ts + 2010);
Calendar value = Calendar.getInstance(tz);
if (y <= 0) {
value.set(Calendar.YEAR, 1 - y);
value.set(Calendar.ERA, GregorianCalendar.BC);
} else {
value.set(Calendar.YEAR, y);
value.set(Calendar.ERA, GregorianCalendar.AD);
}
value.set(Calendar.DAY_OF_YEAR, d);
value.set(Calendar.HOUR_OF_DAY, h);
value.set(Calendar.MINUTE, m);
value.set(Calendar.SECOND, s);
value.set(Calendar.MILLISECOND, u);
return value;
}
private String readString() throws IOException {
if (version >= BundleBinding.VERSION_3) {
return new String(readBytes(0, 0), "UTF-8");
} else {
return in.readUTF();
}
}
private byte[] readBytes(int len, int base) throws IOException {
byte[] bytes = new byte[readVarInt(len, base)];
in.readFully(bytes);
return bytes;
}
}