Author: jukka
Date: Fri Oct 18 15:24:40 2013
New Revision: 1533498

URL: http://svn.apache.org/r1533498
Log:
OAK-987: Implement the MicroKernel API

Quick, first draft of a NodeStore-based MicroKernel

Added:
    
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/NodeStoreKernel.java
    
jackrabbit/oak/trunk/oak-it/mk/src/test/java/org/apache/jackrabbit/mk/test/SegmentMicroKernelFixture.java
Modified:
    
jackrabbit/oak/trunk/oak-it/mk/src/test/resources/META-INF/services/org.apache.jackrabbit.mk.test.MicroKernelFixture

Added: 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/NodeStoreKernel.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/NodeStoreKernel.java?rev=1533498&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/NodeStoreKernel.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/NodeStoreKernel.java
 Fri Oct 18 15:24:40 2013
@@ -0,0 +1,503 @@
+/*
+ * 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.oak.kernel;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static com.google.common.collect.Maps.newConcurrentMap;
+import static com.google.common.collect.Maps.newLinkedHashMap;
+import static org.apache.jackrabbit.oak.commons.PathUtils.getName;
+import static org.apache.jackrabbit.oak.commons.PathUtils.getParentPath;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.jackrabbit.mk.api.MicroKernel;
+import org.apache.jackrabbit.mk.api.MicroKernelException;
+import org.apache.jackrabbit.mk.json.JsopBuilder;
+import org.apache.jackrabbit.mk.json.JsopReader;
+import org.apache.jackrabbit.mk.json.JsopTokenizer;
+import org.apache.jackrabbit.oak.api.Blob;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+
+import com.google.common.io.ByteStreams;
+
+public class NodeStoreKernel implements MicroKernel {
+
+    private final NodeStore store;
+
+    private final Map<String, NodeState> revisions = newLinkedHashMap();
+
+    private final Map<String, NodeBuilder> branches = newLinkedHashMap();
+
+    private final Map<String, Blob> blobs = newConcurrentMap();
+
+    private String head;
+
+    public NodeStoreKernel(NodeStore store) {
+        this.store = store;
+        this.head = UUID.randomUUID().toString();
+        revisions.put(head, store.getRoot());
+    }
+
+    private synchronized NodeState getRoot(String revision)
+            throws MicroKernelException {
+        if (revision == null) {
+            revision = head;
+        }
+
+        NodeState root = revisions.get(revision);
+        if (root != null) {
+            return root;
+        }
+
+        NodeBuilder builder = branches.get(revision);
+        if (builder != null) {
+            return builder.getNodeState();
+        }
+
+        throw new MicroKernelException("Revision not found: " + revision);
+    }
+
+    private NodeState getNode(String revision, String path) {
+        NodeState node = getRoot(revision);
+        for (String element : PathUtils.elements(path)) {
+            node = node.getChildNode(element);
+        }
+        return node;
+    }
+
+    private void applyJsop(NodeBuilder builder, String path, String jsonDiff)
+            throws MicroKernelException {
+        for (String element : PathUtils.elements(path)) {
+            builder = builder.getChildNode(element);
+        }
+        if (builder.exists()) {
+            applyJsop(builder, jsonDiff);
+        } else {
+            throw new MicroKernelException("Path not found: " + path);
+        }
+    }
+
+    private void applyJsop(NodeBuilder builder, String jsonDiff) {
+        JsopTokenizer tokenizer = new JsopTokenizer(jsonDiff);
+        int token = tokenizer.read();
+        while (token != JsopReader.END) {
+            String path = tokenizer.readString();
+            String name = getName(path);
+            switch (token) {
+            case '+':
+                tokenizer.read(':');
+                tokenizer.read('{');
+                NodeBuilder parent = getNode(builder, getParentPath(path));
+                if (builder.hasChildNode(name)) {
+                    throw new MicroKernelException(
+                            "Node already exists: " + path);
+                }
+                addNode(parent.setChildNode(name), tokenizer);
+                break;
+            case '-':
+                getNode(builder, path).remove();
+                break;
+            case '^':
+                tokenizer.read(':');
+                NodeBuilder node = getNode(builder, getParentPath(path));
+                switch (tokenizer.read()) {
+                case JsopReader.NULL:
+                    node.removeProperty(name);
+                    break;
+                case JsopReader.FALSE:
+                    node.setProperty(name, Boolean.FALSE);
+                    break;
+                case JsopReader.TRUE:
+                    node.setProperty(name, Boolean.TRUE);
+                    break;
+                case JsopReader.STRING:
+                    node.setProperty(name, tokenizer.getToken());
+                    break;
+                case JsopReader.NUMBER:
+                    String value = tokenizer.getToken();
+                    try {
+                        node.setProperty(name, Long.parseLong(value));
+                    } catch (NumberFormatException e) {
+                        node.setProperty(name, Double.parseDouble(value));
+                    }
+                    break;
+                default:
+                    throw new UnsupportedOperationException();
+                }
+                break;
+            case '>':
+                throw new UnsupportedOperationException();
+            case '*':
+                throw new UnsupportedOperationException();
+            default:
+                throw new MicroKernelException(
+                        "Unexpected token: " + tokenizer.getEscapedToken());
+            }
+            token = tokenizer.read();
+        }
+    }
+
+    private NodeBuilder getNode(NodeBuilder builder, String path) {
+        for (String element : PathUtils.elements(path)) {
+            builder = builder.getChildNode(element);
+        }
+        if (builder.exists()) {
+            return builder;
+        } else {
+            throw new MicroKernelException("Path not found: " + path);
+        }
+    }
+
+    private void addNode(NodeBuilder builder, JsopTokenizer tokenizer)
+            throws MicroKernelException {
+        if (tokenizer.matches('}')) {
+            return;
+        }
+        do {
+            String name = tokenizer.readString();
+            tokenizer.read(':');
+            switch (tokenizer.read()) {
+            case '{':
+                NodeBuilder child = builder.setChildNode(name);
+                addNode(child, tokenizer);
+                break;
+            case '[':
+                // FIXME: proper array parsing with support for more types
+                List<Long> array = newArrayList();
+                while (tokenizer.matches(JsopReader.NUMBER)) {
+                    array.add(Long.parseLong(tokenizer.getToken()));
+                    tokenizer.matches(',');
+                }
+                tokenizer.read(']');
+                builder.setProperty(name, array, Type.LONGS);
+                break;
+            case JsopReader.FALSE:
+                builder.setProperty(name, Boolean.FALSE);
+                break;
+            case JsopReader.TRUE:
+                builder.setProperty(name, Boolean.TRUE);
+                break;
+            case JsopReader.NUMBER:
+                String value = tokenizer.getToken();
+                try {
+                    builder.setProperty(name, Long.parseLong(value));
+                } catch (NumberFormatException e) {
+                    builder.setProperty(name, Double.parseDouble(value));
+                }
+                break;
+            case JsopReader.STRING:
+                builder.setProperty(name, tokenizer.getToken());
+                break;
+            default:
+                throw new MicroKernelException(
+                        "Unexpected token: " + tokenizer.getEscapedToken());
+            }
+        } while (tokenizer.matches(','));
+        tokenizer.read('}');
+    }
+
+    @Override
+    public synchronized String getHeadRevision() {
+        NodeState root = store.getRoot();
+        if (!root.equals(revisions.get(head))) {
+            head = UUID.randomUUID().toString();
+            revisions.put(head, root);
+            notifyAll();
+        }
+        return head;
+    }
+
+    @Override
+    public String checkpoint(long lifetime) {
+        return getHeadRevision();
+    }
+
+    @Override
+    public synchronized String getRevisionHistory(
+            long since, int maxEntries, String path)
+            throws MicroKernelException {
+        JsopBuilder json = new JsopBuilder();
+        json.array();
+        int count = 0;
+        for (String revision : revisions.keySet()) {
+            if (count++ > maxEntries) {
+                break;
+            }
+            json.object();
+            json.key("id").value(revision);
+            json.endObject();
+        }
+        json.endArray();
+        return json.toString();
+    }
+
+    @Override
+    public synchronized String waitForCommit(
+            String oldHeadRevisionId, long timeout)
+            throws MicroKernelException, InterruptedException {
+        long stop = System.currentTimeMillis() + timeout;
+        while (head.equals(oldHeadRevisionId) && timeout > 0) {
+            wait(timeout);
+            timeout = stop - System.currentTimeMillis();
+        }
+        return head;
+    }
+
+    @Override
+    public String getJournal(
+            String fromRevisionId, String toRevisionId, String path)
+            throws MicroKernelException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public synchronized String diff(
+            String fromRevisionId, String toRevisionId, String path, int depth)
+            throws MicroKernelException {
+        NodeState before = getRoot(fromRevisionId);
+        NodeState after = getRoot(toRevisionId);
+
+        JsopDiff diff = new JsopDiff(null);
+        after.compareAgainstBaseState(before, diff);
+        return diff.toString();
+    }
+
+    @Override
+    public boolean nodeExists(String path, String revisionId)
+            throws MicroKernelException {
+        return getNode(revisionId, path).exists();
+    }
+
+    @Override
+    public long getChildNodeCount(String path, String revisionId)
+            throws MicroKernelException {
+        NodeState node = getNode(revisionId, path);
+        if (node.exists()) {
+            return node.getChildNodeCount(Long.MAX_VALUE);
+        } else {
+            throw new MicroKernelException(
+                    "Node not found: " + revisionId + path);
+        }
+    }
+
+    @Override
+    public String getNodes(
+            String path, String revisionId, int depth,
+            long offset, int maxChildNodes, String filter)
+            throws MicroKernelException {
+        NodeState node = getRoot(revisionId);
+        for (String element : PathUtils.elements(path)) {
+            node = node.getChildNode(element);
+        }
+
+        if (node.exists()) {
+            JsopBuilder json = new JsopBuilder();
+            if (maxChildNodes < 0) {
+                maxChildNodes = Integer.MAX_VALUE;
+            }
+            serialize(getNode(revisionId, path), json, depth, offset, 
maxChildNodes);
+            return json.toString();
+        } else {
+            return null;
+        }
+    }
+
+    private void serialize(
+            NodeState state, JsopBuilder json,
+            int depth, long offset, int maxChildNodes) {
+        json.object();
+
+        for (PropertyState property : state.getProperties()) {
+            json.key(property.getName());
+            Type<?> type = property.getType();
+            if (type.isArray()) {
+                json.array();
+                // FIXME
+                json.endArray();
+            } else if (type == Type.BOOLEAN) {
+                json.value(property.getValue(Type.BOOLEAN).booleanValue());
+            } else if (type == Type.LONG) {
+                json.value(property.getValue(Type.LONG).longValue());
+            } else if (type == Type.DOUBLE) {
+                json.encodedValue(property.getValue(Type.DOUBLE).toString());
+            } else {
+                json.value(property.getValue(Type.STRING));
+            }
+        }
+
+        json.key(":childNodeCount");
+        json.value(state.getChildNodeCount(Long.MAX_VALUE));
+
+        long index = 0;
+        int count = 0;
+        for (ChildNodeEntry entry : state.getChildNodeEntries()) {
+            if (index++ >= offset) {
+                if (count++ >= maxChildNodes) {
+                    break;
+                }
+
+                json.key(entry.getName());
+                if (depth > 0) {
+                    serialize(
+                            entry.getNodeState(), json,
+                            depth - 1, 0, maxChildNodes);
+                } else {
+                    json.object();
+                    json.endObject();
+                }
+            }
+        }
+
+        json.endObject();
+    }
+
+    @Override
+    public synchronized String commit(
+            String path, String jsonDiff, String revisionId, String message)
+            throws MicroKernelException {
+        if (revisionId == null) {
+            revisionId = head;
+        }
+
+        NodeState root = revisions.get(revisionId);
+        if (root != null) {
+            try {
+                NodeBuilder builder = root.builder();
+                applyJsop(builder, path, jsonDiff);
+                NodeState newRoot = store.merge(
+                        builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+                NodeState oldRoot = revisions.get(head);
+                if (!newRoot.equals(oldRoot)) {
+                    String uuid = UUID.randomUUID().toString();
+                    revisions.put(uuid, newRoot);
+                    head = uuid;
+                }
+
+                return head;
+            } catch (CommitFailedException e) {
+                throw new MicroKernelException(e);
+            }
+        }
+
+        NodeBuilder builder = branches.remove(revisionId);
+        if (builder != null) {
+            applyJsop(builder, path, jsonDiff);
+            String uuid = UUID.randomUUID().toString();
+            branches.put(uuid, builder);
+            return uuid;
+        }
+
+        throw new MicroKernelException("Revision not found: " + revisionId);
+    }
+
+    @Override
+    public synchronized String branch(String trunkRevisionId)
+            throws MicroKernelException {
+        String uuid = UUID.randomUUID().toString();
+        branches.put(uuid, getRoot(trunkRevisionId).builder());
+        return uuid;
+    }
+
+    @Override
+    public synchronized String merge(String branchRevisionId, String message)
+            throws MicroKernelException {
+        NodeBuilder builder = branches.remove(branchRevisionId);
+        if (builder != null) {
+            try {
+                NodeState newRoot = store.merge(
+                        builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+                NodeState oldRoot = revisions.get(head);
+                if (!newRoot.equals(oldRoot)) {
+                    String uuid = UUID.randomUUID().toString();
+                    revisions.put(uuid, newRoot);
+                    head = uuid;
+                }
+
+                return head;
+            } catch (CommitFailedException e) {
+                throw new MicroKernelException(e);
+            }
+        } else {
+            throw new MicroKernelException(
+                    "Branch not found: " + branchRevisionId);
+        }
+    }
+
+    @Override
+    public String rebase(String branchRevisionId, String newBaseRevisionId)
+            throws MicroKernelException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public long getLength(String blobId) throws MicroKernelException {
+        Blob blob = blobs.get(blobId);
+        if (blob != null) {
+            return blob.length();
+        } else {
+            throw new MicroKernelException("Blob not found: " + blobId);
+        }
+    }
+
+    @Override
+    public int read(String blobId, long pos, byte[] buff, int off, int length)
+            throws MicroKernelException {
+        Blob blob = blobs.get(blobId);
+        if (blob != null) {
+            try {
+                InputStream stream = blob.getNewStream();
+                try {
+                    ByteStreams.skipFully(stream, pos);
+                    return stream.read(buff, off, length);
+                } finally {
+                    stream.close();
+                }
+            } catch (IOException e) {
+                throw new MicroKernelException("Failed to read a blob", e);
+            }
+        } else {
+            throw new MicroKernelException("Blob not found: " + blobId);
+        }
+    }
+
+    @Override
+    public String write(InputStream in) throws MicroKernelException {
+        try {
+            String uuid = UUID.randomUUID().toString();
+            blobs.put(uuid, store.createBlob(in));
+            return uuid;
+        } catch (IOException e) {
+            throw new MicroKernelException("Failed to create a blob", e);
+        }
+    }
+
+}

Added: 
jackrabbit/oak/trunk/oak-it/mk/src/test/java/org/apache/jackrabbit/mk/test/SegmentMicroKernelFixture.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-it/mk/src/test/java/org/apache/jackrabbit/mk/test/SegmentMicroKernelFixture.java?rev=1533498&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-it/mk/src/test/java/org/apache/jackrabbit/mk/test/SegmentMicroKernelFixture.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-it/mk/src/test/java/org/apache/jackrabbit/mk/test/SegmentMicroKernelFixture.java
 Fri Oct 18 15:24:40 2013
@@ -0,0 +1,49 @@
+/*
+ * 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.mk.test;
+
+import org.apache.jackrabbit.mk.api.MicroKernel;
+import org.apache.jackrabbit.oak.kernel.NodeStoreKernel;
+import org.apache.jackrabbit.oak.plugins.segment.SegmentNodeStore;
+import org.apache.jackrabbit.oak.plugins.segment.memory.MemoryStore;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+
+public class SegmentMicroKernelFixture implements MicroKernelFixture {
+
+    @Override
+    public boolean isAvailable() {
+        return true;
+    }
+
+    @Override
+    public void setUpCluster(MicroKernel[] cluster) {
+        NodeStore store = new SegmentNodeStore(new MemoryStore());
+        MicroKernel mk = new NodeStoreKernel(store);
+        for (int i = 0; i < cluster.length; i++) {
+            cluster[i] = mk;
+        }
+    }
+
+    @Override
+    public void syncMicroKernelCluster(MicroKernel... nodes) {
+    }
+
+    @Override
+    public void tearDownCluster(MicroKernel[] cluster) {
+    }
+
+}

Modified: 
jackrabbit/oak/trunk/oak-it/mk/src/test/resources/META-INF/services/org.apache.jackrabbit.mk.test.MicroKernelFixture
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-it/mk/src/test/resources/META-INF/services/org.apache.jackrabbit.mk.test.MicroKernelFixture?rev=1533498&r1=1533497&r2=1533498&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-it/mk/src/test/resources/META-INF/services/org.apache.jackrabbit.mk.test.MicroKernelFixture
 (original)
+++ 
jackrabbit/oak/trunk/oak-it/mk/src/test/resources/META-INF/services/org.apache.jackrabbit.mk.test.MicroKernelFixture
 Fri Oct 18 15:24:40 2013
@@ -17,3 +17,4 @@ org.apache.jackrabbit.mk.test.MicroKerne
 org.apache.jackrabbit.mk.test.ClientServerFixture
 #org.apache.jackrabbit.mk.test.MongoMicroKernelFixture
 #org.apache.jackrabbit.mk.test.MongoGridFSMicroKernelFixture
+#org.apache.jackrabbit.mk.test.SegmentMicroKernelFixture


Reply via email to