jackrabbit-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From thom...@apache.org
Subject 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
Date Thu, 22 Aug 2013 09:33:40 GMT
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);
+    }
+
+}



Mime
View raw message