You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jackrabbit.apache.org by th...@apache.org on 2013/08/22 11:33:40 UTC
svn commit: r1516397 - in
/jackrabbit/branches/2.4/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util:
BundleBinding.java BundleReaderSlower.java BundleWriterSlower.java
Author: thomasm
Date: Thu Aug 22 09:33:40 2013
New Revision: 1516397
URL: http://svn.apache.org/r1516397
Log:
JCR-3652 Bundle serialization broken due to missing byte for boolean property - workaround using the "jackrabbit.verifyBundles" system property
Added:
jackrabbit/branches/2.4/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleReaderSlower.java
jackrabbit/branches/2.4/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleWriterSlower.java
Modified:
jackrabbit/branches/2.4/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleBinding.java
Modified: jackrabbit/branches/2.4/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleBinding.java
URL: http://svn.apache.org/viewvc/jackrabbit/branches/2.4/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleBinding.java?rev=1516397&r1=1516396&r2=1516397&view=diff
==============================================================================
--- jackrabbit/branches/2.4/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleBinding.java (original)
+++ jackrabbit/branches/2.4/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleBinding.java Thu Aug 22 09:33:40 2013
@@ -16,6 +16,8 @@
*/
package org.apache.jackrabbit.core.persistence.util;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -23,12 +25,28 @@ import java.io.OutputStream;
import org.apache.jackrabbit.core.data.DataStore;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.util.StringIndex;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* This Class implements efficient serialization methods for item states.
*/
public class BundleBinding {
+ private static boolean VERIFY_BUNDLES = Boolean.getBoolean("jackrabbit.verifyBundles");
+
+ private static boolean ALLOW_BROKEN_BUNDLES = Boolean.getBoolean("jackrabbit.allowBrokenBundleWrites");
+
+ private static Logger log = LoggerFactory.getLogger(BundleBinding.class);
+
+ static {
+ if (VERIFY_BUNDLES) {
+ log.warn("Please note reading and writing bundles is slightly slower " +
+ "because the system property \"jackrabbit.verifyBundles\" is enabled. " +
+ "See JCR-3652.");
+ }
+ }
+
static final int BINARY_IN_BLOB_STORE = -1;
static final int BINARY_IN_DATA_STORE = -2;
@@ -149,6 +167,9 @@ public class BundleBinding {
*/
public NodePropBundle readBundle(InputStream in, NodeId id)
throws IOException {
+ if (VERIFY_BUNDLES) {
+ return new BundleReaderSlower(this, in).readBundle(id);
+ }
return new BundleReader(this, in).readBundle(id);
}
@@ -161,7 +182,53 @@ public class BundleBinding {
*/
public void writeBundle(OutputStream out, NodePropBundle bundle)
throws IOException {
+ if (VERIFY_BUNDLES) {
+ writeBundleSlower(out, bundle);
+ return;
+ }
new BundleWriter(this, out).writeBundle(bundle);
}
+
+ private void writeBundleSlower(OutputStream out, NodePropBundle bundle)
+ throws IOException {
+ byte[] data;
+ IOException lastError = null;
+ for (int i = 0; i < 5; i++) {
+ ByteArrayOutputStream buff = new ByteArrayOutputStream();
+ if (i < 3) {
+ new BundleWriter(this, buff).writeBundle(bundle);
+ } else {
+ log.warn("Corrupt bundle: writing slower, i = " + i);
+ new BundleWriterSlower(this, buff).writeBundle(bundle);
+ }
+ data = buff.toByteArray();
+ NodeId id = bundle.getId();
+ try {
+ ByteArrayInputStream in = new ByteArrayInputStream(data);
+ new BundleReaderSlower(this, in).readBundleNormal(id);
+ } catch (IOException e) {
+ lastError = e;
+ log.warn("Corrupt bundle: could not read the bundle I wrote", e);
+ ByteArrayInputStream in = new ByteArrayInputStream(data);
+ try {
+ new BundleReader(this, in).readBundle(id);
+ log.warn("Corrupt bundle: can fix error while reading");
+ if (i == 4 && ALLOW_BROKEN_BUNDLES) {
+ log.warn("Corrupt bundle: writing broken one");
+ break;
+ }
+ } catch (IOException e2) {
+ lastError = e2;
+ log.warn("Corrupt bundle: could not read even when trying harder", e2);
+ }
+ continue;
+ }
+ out.write(data);
+ return;
+ }
+ String msg = "Corrupt bundle: could not write a correct bundle, giving up: " + bundle;
+ log.error(msg);
+ throw new IOException(msg, lastError);
+ }
}
Added: jackrabbit/branches/2.4/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleReaderSlower.java
URL: http://svn.apache.org/viewvc/jackrabbit/branches/2.4/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleReaderSlower.java?rev=1516397&view=auto
==============================================================================
--- jackrabbit/branches/2.4/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleReaderSlower.java (added)
+++ jackrabbit/branches/2.4/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleReaderSlower.java Thu Aug 22 09:33:40 2013
@@ -0,0 +1,955 @@
+/*
+ * 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.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.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.SequenceInputStream;
+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, slow but "careful". See the {@link BundleWriter} class for details of
+ * the serialization format.
+ *
+ * @see BundleWriter
+ */
+class BundleReaderSlower {
+
+ /*
+ * Implementation note: if you change this class, also change BundleDumper
+ * accordingly.
+ */
+
+ /** Logger instance */
+ private static Logger log = LoggerFactory.getLogger(BundleReaderSlower.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;
+
+ private BufferedInputStream bufferedIn;
+
+ /**
+ * Counter for the number of bytes read from the input stream.
+ */
+ private CountingInputStream cin;
+
+ /**
+ * Wrapper for reading structured data from the input stream.
+ */
+ private DataInputStream in;
+
+ private 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 String[] namespaces;
+
+ /**
+ * 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 BundleReaderSlower(BundleBinding binding, InputStream stream)
+ throws IOException {
+ this.binding = binding;
+ this.bufferedIn = new BufferedInputStream(stream);
+ bufferedIn.mark(1024 * 1024);
+ reset();
+ }
+
+ void reset() throws IOException {
+ bufferedIn.reset();
+ bufferedIn.mark(1024 * 1024);
+ this.cin = new CountingInputStream(bufferedIn);
+ this.in = new DataInputStream(cin);
+ this.version = in.readUnsignedByte();
+ namespaces = new String[]
+ // NOTE: The length of this array must be seven
+ { Name.NS_DEFAULT_URI, null, null, null, null, null, null };
+ }
+
+ /**
+ * 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 {
+ try {
+ return readBundleNormal(id);
+ } catch (IOException e) {
+ log.warn("Corrupt bundle: exception trying to read bundle " + id + ", trying again", e);
+ try {
+ reset();
+ return readBundleTryHarder(id);
+ } catch (RetryException e2) {
+ int pos = (int) e2.getErrorPosition();
+ log.warn("Corrupt bundle: exception trying to read bundle " + id + ", retry at " + pos, e);
+ bufferedIn.reset();
+ log.warn("Corrupt bundle: inserting 1 at position " + pos);
+ byte[] before = new byte[pos + 2];
+ DataInputStream rf = new DataInputStream(bufferedIn);
+ rf.readFully(before, 0, pos + 1);
+ before[pos + 1] = before[pos];
+ before[pos] = (byte) 1;
+ SequenceInputStream seq = new SequenceInputStream(
+ new ByteArrayInputStream(before), rf);
+ bufferedIn = new BufferedInputStream(seq);
+ bufferedIn.mark(1024 * 1024);
+ reset();
+ log.warn("Corrupt bundle: re-trying");
+ try {
+ return readBundleTryHarder(id);
+ } catch (IOException e3) {
+ log.error("Corrupt bundle: exception trying to read bundle " + id + ", giving up (2)", e3);
+ throw e3;
+ }
+ } catch (IOException e2) {
+ log.error("Corrupt bundle: exception trying to read bundle " + id + ", giving up (1)", e2);
+ throw e2;
+ }
+ }
+ }
+
+ public NodePropBundle readBundleNormal(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 NodePropBundle readBundleTryHarder(NodeId id) throws IOException {
+ long start = cin.getByteCount();
+ NodePropBundle bundle = new NodePropBundle(id);
+ if (version >= BundleBinding.VERSION_3) {
+ readBundleNewTryHarder(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 readBundleNewTryHarder(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(readPropertyEntryTryHarder(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;
+ switch (entry.getType()) {
+ 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 IOException("Unable to create property value: " + e.toString());
+ }
+ } 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 <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 readPropertyEntryTryHarder(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;
+ switch (entry.getType()) {
+ 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 IOException("Unable to create property value: " + e.toString());
+ }
+ } 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:
+ int b = in.read();
+ if (b == 0 || b == 1) {
+ val = InternalValue.create(b == 1);
+ } else {
+ throw new RetryException(cin.getByteCount() - 1);
+ }
+ 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;
+ }
+
+ static class RetryException extends IOException {
+
+ final long errorPosition;
+
+ RetryException(long errorPosition) {
+ this.errorPosition = errorPosition;
+ }
+
+ long getErrorPosition() {
+ return errorPosition;
+ }
+
+ }
+
+}
Added: jackrabbit/branches/2.4/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleWriterSlower.java
URL: http://svn.apache.org/viewvc/jackrabbit/branches/2.4/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleWriterSlower.java?rev=1516397&view=auto
==============================================================================
--- jackrabbit/branches/2.4/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleWriterSlower.java (added)
+++ jackrabbit/branches/2.4/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleWriterSlower.java Thu Aug 22 09:33:40 2013
@@ -0,0 +1,721 @@
+/*
+ * 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 java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.math.BigDecimal;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.GregorianCalendar;
+
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.jackrabbit.core.id.NodeId;
+import org.apache.jackrabbit.core.value.InternalValue;
+import org.apache.jackrabbit.core.persistence.util.NodePropBundle.ChildNodeEntry;
+import org.apache.jackrabbit.core.persistence.util.NodePropBundle.PropertyEntry;
+import org.apache.jackrabbit.spi.Name;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Bundle slow but "careful" serializer. Careful specially for boolean properties.
+ *
+ * @see BundleReader
+ */
+class BundleWriterSlower {
+
+ /** Logger instance */
+ private static Logger log = LoggerFactory.getLogger(BundleWriterSlower.class);
+
+ private final BundleBinding binding;
+
+ private final DataOutputStream out;
+
+ /**
+ * The default namespace and the first six other namespaces used in this
+ * bundle. Used by the {@link #writeName(Name)} 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 serializer.
+ *
+ * @param binding bundle binding
+ * @param stream stream to which the bundle will be written
+ * @throws IOException if an I/O error occurs.
+ */
+ public BundleWriterSlower(BundleBinding binding, OutputStream stream)
+ throws IOException {
+ assert namespaces.length == 7;
+ this.binding = binding;
+ this.out = new DataOutputStream(stream);
+ this.out.writeByte(BundleBinding.VERSION_CURRENT);
+ }
+
+ /**
+ * Serializes a <code>NodePropBundle</code> to a data output stream
+ *
+ * @param bundle the bundle to serialize
+ * @throws IOException if an I/O error occurs.
+ */
+ public void writeBundle(NodePropBundle bundle)
+ throws IOException {
+ long size = out.size();
+
+ // primaryType
+ writeName(bundle.getNodeTypeName());
+
+ // parentUUID
+ NodeId parentId = bundle.getParentId();
+ if (parentId == null) {
+ parentId = BundleBinding.NULL_PARENT_ID;
+ }
+ writeNodeId(parentId);
+
+ // write mod count
+ writeVarInt(bundle.getModCount());
+
+ Collection<Name> mixins = bundle.getMixinTypeNames();
+ Collection<PropertyEntry> properties = bundle.getPropertyEntries();
+ Collection<ChildNodeEntry> nodes = bundle.getChildNodeEntries();
+ Collection<NodeId> shared = bundle.getSharedSet();
+
+ int mn = mixins.size();
+ int pn = properties.size();
+ int nn = nodes.size();
+ int sn = shared.size();
+ int referenceable = 0;
+ if (bundle.isReferenceable()) {
+ referenceable = 1;
+ }
+ out.writeByte(
+ Math.min(mn, 1) << 7
+ | Math.min(pn, 7) << 4
+ | Math.min(nn, 3) << 2
+ | Math.min(sn, 1) << 1
+ | referenceable);
+
+ // mixin types
+ writeVarInt(mn, 1);
+ for (Name name : mixins) {
+ writeName(name);
+ }
+
+ // properties
+ writeVarInt(pn, 7);
+ for (PropertyEntry property : properties) {
+ writeState(property);
+ }
+
+ // child nodes (list of name/uuid pairs)
+ writeVarInt(nn, 3);
+ for (ChildNodeEntry child : nodes) {
+ writeName(child.getName()); // name
+ writeNodeId(child.getId()); // uuid
+ }
+
+ // write shared set
+ writeVarInt(sn, 1);
+ for (NodeId nodeId: shared) {
+ writeNodeId(nodeId);
+ }
+
+ // set size of bundle
+ bundle.setSize(out.size() - size);
+ }
+
+ /**
+ * Serializes a property entry. The serialization begins with the
+ * property name followed by a single byte that encodes the type and
+ * multi-valuedness of the property:
+ * <pre>
+ * +-------------------------------+
+ * | mv count | type |
+ * +-------------------------------+
+ * </pre>
+ * <p>
+ * The lower four bits encode the property type (0-12 in JCR 2.0) and
+ * higher bits indicate whether this is a multi-valued property and how
+ * many property values there are. A value of 0 is reserved for
+ * single-valued properties (that are guaranteed to always have just a
+ * single value), and all non-zero values indicate a multi-valued property.
+ * <p>
+ * In multi-valued properties the exact value of the "mv count" field is
+ * the number of property values plus one and truncated at 15 (the highest
+ * four-bit value). If there are 14 or more (14 + 1 == 15) property values,
+ * then the number of additional values is serialized as a variable-length
+ * integer (see {@link #writeVarInt(int)}) right after this byte.
+ * <p>
+ * The modification count of the property state is written next as a
+ * variable-length integer, followed by the serializations of all the
+ * values of this property.
+ *
+ * @param state the property entry to store
+ * @throws IOException if an I/O error occurs.
+ */
+ private void writeState(NodePropBundle.PropertyEntry state)
+ throws IOException {
+ log.info("writing state for property " + state.getName());
+ writeName(state.getName());
+
+ InternalValue[] values = state.getValues();
+
+ int type = state.getType();
+ log.info("writing type " + type);
+ assert 0 <= type && type <= 0x0f;
+ if (state.isMultiValued()) {
+ log.info("writing multi-valued");
+ int len = values.length + 1;
+ if (len < 0x0f) {
+ out.writeByte(len << 4 | type);
+ } else {
+ out.writeByte(0xf0 | type);
+ writeVarInt(len - 0x0f);
+ }
+ } else {
+ assert values.length == 1;
+ out.writeByte(type);
+ }
+
+ writeVarInt(state.getModCount());
+
+ // values
+ for (int i = 0; i < values.length; i++) {
+ InternalValue val = values[i];
+ switch (state.getType()) {
+ case PropertyType.BINARY:
+ try {
+ long size = val.getLength();
+ if (val.isInDataStore()) {
+ out.writeInt(BundleBinding.BINARY_IN_DATA_STORE);
+ writeString(val.toString());
+ } else if (binding.dataStore != null) {
+ writeSmallBinary(val, state, i);
+ } else if (size < 0) {
+ log.warn("Blob has negative size. Potential loss of data. "
+ + "id={} idx={}", state.getId(), String.valueOf(i));
+ out.writeInt(0);
+ values[i] = InternalValue.create(new byte[0]);
+ val.discard();
+ } else if (size > binding.getMinBlobSize()) {
+ // special handling required for binary value:
+ // spool binary value to file in blob store
+ out.writeInt(BundleBinding.BINARY_IN_BLOB_STORE);
+ String blobId = state.getBlobId(i);
+ if (blobId == null) {
+ BLOBStore blobStore = binding.getBlobStore();
+ try {
+ InputStream in = val.getStream();
+ try {
+ blobId = blobStore.createId(state.getId(), i);
+ blobStore.put(blobId, in, size);
+ state.setBlobId(blobId, i);
+ } finally {
+ IOUtils.closeQuietly(in);
+ }
+ } catch (Exception e) {
+ String msg = "Error while storing blob. id="
+ + state.getId() + " idx=" + i + " size=" + size;
+ log.error(msg, e);
+ throw new IOException(msg);
+ }
+ try {
+ // replace value instance with value
+ // backed by resource in blob store and delete temp file
+ if (blobStore instanceof ResourceBasedBLOBStore) {
+ values[i] = InternalValue.create(((ResourceBasedBLOBStore) blobStore).getResource(blobId));
+ } else {
+ values[i] = InternalValue.create(blobStore.get(blobId));
+ }
+ } catch (Exception e) {
+ log.error("Error while reloading blob. truncating. id="
+ + state.getId() + " idx=" + i + " size=" + size, e);
+ values[i] = InternalValue.create(new byte[0]);
+ }
+ val.discard();
+ }
+ // store id of blob as property value
+ writeString(blobId); // value
+ } else {
+ // delete evt. blob
+ byte[] data = writeSmallBinary(val, state, i);
+ // replace value instance with value
+ // backed by resource in blob store and delete temp file
+ values[i] = InternalValue.create(data);
+ val.discard();
+ }
+ } catch (RepositoryException e) {
+ String msg = "Error while storing blob. id="
+ + state.getId() + " idx=" + i + " value=" + val;
+ log.error(msg, e);
+ throw new IOException(msg);
+ }
+ break;
+ case PropertyType.DOUBLE:
+ try {
+ out.writeDouble(val.getDouble());
+ } catch (RepositoryException e) {
+ // should never occur
+ throw new IOException("Unexpected error while writing DOUBLE value.");
+ }
+ break;
+ case PropertyType.DECIMAL:
+ try {
+ writeDecimal(val.getDecimal());
+ } catch (RepositoryException e) {
+ // should never occur
+ throw new IOException("Unexpected error while writing DECIMAL value.");
+ }
+ break;
+ case PropertyType.LONG:
+ try {
+ writeVarLong(val.getLong());
+ } catch (RepositoryException e) {
+ // should never occur
+ throw new IOException("Unexpected error while writing LONG value.");
+ }
+ break;
+ case PropertyType.BOOLEAN:
+ log.info("Writing boolean");
+ try {
+ boolean value = val.getBoolean();
+ out.flush();
+ out.writeBoolean(value);
+ log.info("Wrote boolean " + value);
+ out.flush();
+ } catch (RepositoryException e) {
+ // should never occur
+ throw new IOException("Unexpected error while writing BOOLEAN value.");
+ }
+ break;
+ case PropertyType.NAME:
+ try {
+ writeName(val.getName());
+ } catch (RepositoryException e) {
+ // should never occur
+ throw new IOException("Unexpected error while writing NAME value.");
+ }
+ break;
+ case PropertyType.WEAKREFERENCE:
+ case PropertyType.REFERENCE:
+ writeNodeId(val.getNodeId());
+ break;
+ case PropertyType.DATE:
+ try {
+ writeDate(val.getCalendar());
+ } catch (RepositoryException e) {
+ // should never occur
+ throw new IOException("Unexpected error while writing DATE value.");
+ }
+ break;
+ default:
+ writeString(val.toString());
+ break;
+ }
+ }
+ }
+
+ /**
+ * Write a small binary value and return the data.
+ *
+ * @param value the binary value
+ * @param state the property state (for error messages)
+ * @param i the index (for error messages)
+ * @return the data
+ * @throws IOException if the data could not be read
+ */
+ private byte[] writeSmallBinary(
+ InternalValue value, NodePropBundle.PropertyEntry state, int i)
+ throws IOException {
+ try {
+ int size = (int) value.getLength();
+ out.writeInt(size);
+ byte[] data = new byte[size];
+ DataInputStream in =
+ new DataInputStream(value.getStream());
+ try {
+ in.readFully(data);
+ } finally {
+ IOUtils.closeQuietly(in);
+ }
+ out.write(data, 0, data.length);
+ return data;
+ } catch (Exception e) {
+ String msg = "Error while storing blob. id="
+ + state.getId() + " idx=" + i + " value=" + value;
+ log.error(msg, e);
+ throw new IOException(msg);
+ }
+ }
+
+ /**
+ * Serializes a node identifier
+ *
+ * @param id the node id
+ * @throws IOException in an I/O error occurs.
+ */
+ private void writeNodeId(NodeId id) throws IOException {
+ out.writeLong(id.getMostSignificantBits());
+ out.writeLong(id.getLeastSignificantBits());
+ }
+
+ /**
+ * Serializes a BigDecimal
+ *
+ * @param decimal the decimal number
+ * @throws IOException in an I/O error occurs.
+ */
+ private void writeDecimal(BigDecimal decimal) throws IOException {
+ if (decimal == null) {
+ out.writeBoolean(false);
+ } else {
+ out.writeBoolean(true);
+ // TODO more efficient serialization format
+ writeString(decimal.toString());
+ }
+ }
+
+ /**
+ * Serializes a name. The name encoding works as follows:
+ * <p>
+ * First; if the name is known by the {@link BundleNames} class (this
+ * includes the <code>null</code> name), then the name is serialized
+ * as a single byte using the following format.
+ * <pre>
+ * +-------------------------------+
+ * | 0 | common name index |
+ * +-------------------------------+
+ * </pre>
+ * <p>
+ * Second; if the name is not known, it gets serialized as a
+ * variable-length field whose first byte looks like this:
+ * <pre>
+ * +-------------------------------+
+ * | 1 | ns index | name length |
+ * +-------------------------------+
+ * </pre>
+ * <p>
+ * The three-bit namespace index identifies the namespace of the name.
+ * The serializer keeps track of the default namespace (value 0) and at
+ * most six other other namespaces (values 1-6), in the order they appear
+ * in the bundle. When one of these six custom namespaces first appears
+ * in the bundle, then the namespace URI is written using
+ * {@link #writeString(String)} right after this byte.
+ * Later uses of such a namespace simply refers back to the already read
+ * namespace URI string. Any other namespaces are identified with value 7
+ * and always written to the bundle after this byte.
+ * <p>
+ * The four-bit name length field indicates the length (in UTF-8 bytes)
+ * of the local part of the name. Since zero-length local names are not
+ * allowed, the length is first decremented by one before storing in this
+ * field. The UTF-8 byte sequence is written out after this byte and the
+ * possible namespace URI string. If the length of the local name is
+ * larger than 15 (i.e. would be stored as 0x0f or more), then the value
+ * 0x0f is stored as the name length and the name string is written as
+ * UTF-8 using {@link #writeBytes(byte[], int)} with a base length of
+ * 0x10 (0x0f + 1).
+ *
+ * @param name the name
+ * @throws IOException in an I/O error occurs.
+ */
+ private void writeName(Name name) throws IOException {
+ int index = BundleNames.nameToIndex(name);
+ if (index != -1) {
+ assert 0 <= index && index < 0x80;
+ out.writeByte(index);
+ } else {
+ String uri = name.getNamespaceURI();
+ int ns = 0;
+ while (ns < namespaces.length
+ && namespaces[ns] != null
+ && !namespaces[ns].equals(uri)) {
+ ns++;
+ }
+
+ String local = name.getLocalName();
+ byte[] bytes = local.getBytes("UTF-8");
+ int len = Math.min(bytes.length - 1, 0x0f);
+
+ out.writeByte(0x80 | ns << 4 | len);
+ if (ns == namespaces.length || namespaces[ns] == null) {
+ writeString(uri);
+ if (ns < namespaces.length) {
+ namespaces[ns] = uri;
+ }
+ }
+ if (len != 0x0f) {
+ out.write(bytes);
+ } else {
+ writeBytes(bytes, 0x0f + 1);
+ }
+ }
+ }
+
+ /**
+ * Serializes an integer using a variable-length encoding that favors
+ * small positive numbers. The serialization consists of one to five
+ * bytes of the following format:
+ * <pre>
+ * +-------------------------------+
+ * | c | 7 least significant bits |
+ * +-------------------------------+
+ * </pre>
+ * <p>
+ * If the given integer fits in seven bits (i.e. the value between
+ * 0 and 127, inclusive), then it is written as-is in a single byte.
+ * Otherwise the continuation flag <code>c</code> is set and the least
+ * significant seven bits are written together with the flag as a single
+ * byte. The integer is then shifed right seven bits and the process
+ * continues from the beginning.
+ * <p>
+ * This format uses a single byte for values 0-127, two bytes for
+ * 128-16343, three for 16343-2097151, four for 2097152-268435455
+ * and five bytes for all other 32-bit numbers (including negative ones).
+ *
+ * @param integer integer value
+ * @throws IOException if an I/O error occurs
+ */
+ private void writeVarInt(int value) throws IOException {
+ while (true) {
+ int b = value & 0x7f;
+ if (b != value) {
+ out.writeByte(b | 0x80);
+ value >>>= 7; // unsigned shift
+ } else {
+ out.writeByte(b);
+ return;
+ }
+ }
+ }
+
+ private void writeVarInt(int value, int base) throws IOException {
+ if (value >= base) {
+ writeVarInt(value - base);
+ }
+ }
+
+ /**
+ * Serializes a long value using a variable length encoding like the
+ * one used by {@link #writeVarInt(int)} for integer values. Before
+ * writing out, the value is first normalized to an unsigned value
+ * by moving the sign bit to be the end negating the other bits of
+ * a negative value. This normalization step maximizes the number of
+ * zero high order bits for typical small values (positive or negative),
+ * and thus keeps the serialization short.
+ *
+ * @param value long value
+ * @throws IOException if an I/O error occurs
+ */
+ private void writeVarLong(long value) throws IOException {
+ // Normalize to an unsigned value with the sign as the lowest bit
+ if (value < 0) {
+ value = ~value << 1 | 1;
+ } else {
+ value <<= 1;
+ }
+ while (true) {
+ long b = value & 0x7f;
+ if (b != value) {
+ out.writeByte((int) b | 0x80);
+ value >>>= 7; // unsigned shift
+ } else {
+ out.writeByte((int) b);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Serializes a JCR date value using the {@link #writeVarLong(long)}
+ * serialization on a special 64-bit date encoding. This encoding maps
+ * the <code>sYYYY-MM-DDThh:mm:ss.sssTZD</code> date format used by
+ * JCR to an as small 64 bit integer (positive or negative) as possible,
+ * while preserving full accuracy (including time zone offsets) and
+ * favouring common levels of accuracy (per minute, hour and day) over
+ * full millisecond level detail.
+ * <p>
+ * Each date value is mapped to separate timestamp and timezone fields,
+ * both of whose lenghts are variable:
+ * <pre>
+ * +----- ... ------- ... --+
+ * | timestamp | timezone |
+ * +----- ... ------- ... --+
+ * </pre>
+ * <p>
+ * The type and length of the timezone field can be determined by looking
+ * at the two least significant bits of the value:
+ * <dl>
+ * <dt><code>?0</code></dt>
+ * <dd>
+ * UTC time. The length of the timezone field is just one bit,
+ * i.e. the second bit is already a part of the timestamp field.
+ * </dd>
+ * <dt><code>01</code></dt>
+ * <dd>
+ * The offset is counted as hours from UTC, and stored as the number
+ * of hours (positive or negative) in the next 5 bits (range from
+ * -16 to +15 hours), making the timezone field 7 bits long in total.
+ * </dd>
+ * <dt><code>11</code></dt>
+ * <dd>
+ * The offset is counted as hours and minutes from UTC, and stored
+ * as the total minute offset (positive or negative) in the next
+ * 11 bits (range from -17 to +17 hours), making the timezone field
+ * 13 bits long in total.
+ * </dd>
+ * </dl>
+ * <p>
+ * The remaining 51-63 bits of the encoded value make up the timestamp
+ * field that also uses the two least significant bits to indicate the
+ * type and length of the field:
+ * <dl>
+ * <dt><code>00</code></dt>
+ * <dd>
+ * <code>sYYYY-MM-DDT00:00:00.000</code>, i.e. midnight of the
+ * specified date. The next 9 bits encode the day within the year
+ * (starting from 1, maximum value 366) and the remaining bits are
+ * used for the year, stored as an offset from year 2010.
+ * </dd>
+ * <dt><code>01</code></dt>
+ * <dd>
+ * <code>sYYYY-MM-DDThh:00:00.000</code>, i.e. at the hour. The
+ * next 5 bits encode the hour within the day (starting from 0,
+ * maximum value 23) and the remaining bits are used as described
+ * above for the date.
+ * </dd>
+ * <dt><code>10</code></dt>
+ * <dd>
+ * <code>sYYYY-MM-DDThh:mm:00.000</code>, i.e. at the minute. The
+ * next 11 bits encode the minute within the day (starting from 0,
+ * maximum value 1439) and the remaining bits are used as described
+ * above for the date.
+ * </dd>
+ * <dt><code>11</code></dt>
+ * <dd>
+ * <code>sYYYY-MM-DDThh:mm:ss.sss</code>, i.e. full millisecond
+ * accuracy. The next 30 bits encode the millisecond within the
+ * day (starting from 0, maximum value 87839999) and the remaining
+ * bits are used as described above for the date.
+ * </dd>
+ * </dl>
+ * <p>
+ * With full timezone and millisecond accuracies, this encoding leaves
+ * 10 bits (64 - 9 - 30 - 2 - 11 - 2) for the date offset, which allows
+ * for representation of all timestamps between years 1498 and 2521.
+ * Timestamps outside this range and with a minute-level timezone offset
+ * are automatically truncated to minute-level accuracy to support the
+ * full range of years -9999 to 9999 specified in JCR.
+ * <p>
+ * Note that the year, day of year, and time of day values are stored
+ * as separate bit sequences to avoid problems with changing leap second
+ * or leap year definitions. Bit fields are used for better encoding and
+ * decoding performance than what would be possible with the slightly more
+ * space efficient mechanism of using multiplication and modulo divisions
+ * to separate the different timestamp fields.
+ *
+ * @param value date value
+ * @throws IOException if an I/O error occurs
+ */
+ private void writeDate(Calendar value) throws IOException {
+ int y = value.get(Calendar.YEAR);
+ if (value.isSet(Calendar.ERA)
+ && value.get(Calendar.ERA) == GregorianCalendar.BC) {
+ y = 1 - y; // convert to an astronomical year
+ }
+ y -= 2010; // use a recent offset NOTE: do not change this!
+
+ int d = value.get(Calendar.DAY_OF_YEAR);
+ int h = value.get(Calendar.HOUR_OF_DAY);
+ int m = value.get(Calendar.MINUTE);
+ int s = value.get(Calendar.SECOND);
+ int u = value.get(Calendar.MILLISECOND);
+ int z = value.getTimeZone().getOffset(value.getTimeInMillis()) / (60 * 1000);
+ int zh = z / 60;
+ int zm = z - zh * 60;
+
+ long ts = y << 9 | d & 0x01ff;
+
+ if ((u != 0 || s != 0) && ((-512 <= y && y < 512) || zm == 0)) {
+ ts <<= 30;
+ ts |= (((h * 60 + m) * 60 + s) * 1000 + u) & 0x3fffffff; // 30 bits
+ ts <<= 2;
+ ts |= 3;
+ } else if (m != 0) {
+ ts <<= 11;
+ ts |= (h * 60 + m) & 0x07ff; // 11 bits
+ ts <<= 2;
+ ts |= 2;
+ } else if (h != 0) {
+ ts <<= 5;
+ ts |= h & 0x1f; // 5 bits
+ ts <<= 2;
+ ts |= 1;
+ } else {
+ ts <<= 2;
+ }
+
+ if (zm != 0) {
+ ts <<= 11;
+ ts |= z & 0x07ff; // 11 bits
+ writeVarLong(ts << 2 | 3);
+ } else if (zh != 0) {
+ ts <<= 5;
+ ts |= zh & 0x1f; // 5 bits
+ writeVarLong(ts << 2 | 1);
+ } else {
+ writeVarLong(ts << 1);
+ }
+ }
+
+ /**
+ * Serializes a string in UTF-8. The length of the UTF-8 byte sequence
+ * is first written as a variable-length string (see
+ * {@link #writeVarInt(int)}), and then the sequence itself is written.
+ *
+ * @param value string value
+ * @throws IOException if an I/O error occurs
+ */
+ private void writeString(String value) throws IOException {
+ writeBytes(value.getBytes("UTF-8"), 0);
+ }
+
+ /**
+ * Serializes the given array of bytes. The length of the byte array is
+ * first written as a {@link #writeVarInt(int) variable length integer},
+ * followed by the given bytes.
+ *
+ * @param bytes the bytes to be serialized
+ * @param base optional base length
+ * @throws IOException if an I/O error occurs
+ */
+ private void writeBytes(byte[] bytes, int base) throws IOException {
+ assert bytes.length >= base;
+ writeVarInt(bytes.length - base);
+ out.write(bytes);
+ }
+
+}