You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by md...@apache.org on 2015/06/11 14:09:17 UTC

svn commit: r1684861 [3/8] - in /jackrabbit/oak/trunk: ./ oak-remote/ oak-remote/src/ oak-remote/src/main/ oak-remote/src/main/java/ oak-remote/src/main/java/org/ oak-remote/src/main/java/org/apache/ oak-remote/src/main/java/org/apache/jackrabbit/ oak-...

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteSession.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteSession.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteSession.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteSession.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,454 @@
+/*
+ * 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.remote.content;
+
+import org.apache.jackrabbit.oak.api.Blob;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.ContentSession;
+import org.apache.jackrabbit.oak.api.PropertyValue;
+import org.apache.jackrabbit.oak.api.Result;
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.remote.RemoteBinaryFilters;
+import org.apache.jackrabbit.oak.remote.RemoteBinaryId;
+import org.apache.jackrabbit.oak.remote.RemoteCommitException;
+import org.apache.jackrabbit.oak.remote.RemoteOperation;
+import org.apache.jackrabbit.oak.remote.RemoteQueryParseException;
+import org.apache.jackrabbit.oak.remote.RemoteResults;
+import org.apache.jackrabbit.oak.remote.RemoteRevision;
+import org.apache.jackrabbit.oak.remote.RemoteSession;
+import org.apache.jackrabbit.oak.remote.RemoteTreeFilters;
+import org.apache.jackrabbit.oak.remote.RemoteValue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.jackrabbit.oak.commons.PathUtils.denotesRoot;
+import static org.apache.jackrabbit.oak.commons.PathUtils.isAbsolute;
+import static org.apache.jackrabbit.oak.commons.PathUtils.isAncestor;
+
+class ContentRemoteSession implements RemoteSession {
+
+    private final ContentSession contentSession;
+
+    private final ContentRemoteRevisions contentRemoteRevisions;
+
+    private final ContentRemoteBinaries contentRemoteBinaries;
+
+    public ContentRemoteSession(ContentSession contentSession, ContentRemoteRevisions contentRemoteRevisions, ContentRemoteBinaries contentRemoteBinaries) {
+        this.contentSession = contentSession;
+        this.contentRemoteRevisions = contentRemoteRevisions;
+        this.contentRemoteBinaries = contentRemoteBinaries;
+    }
+
+    @Override
+    public ContentRemoteRevision readLastRevision() {
+        Root root = contentSession.getLatestRoot();
+        String revisionId = contentRemoteRevisions.put(contentSession.getAuthInfo(), root);
+        return new ContentRemoteRevision(revisionId, root);
+    }
+
+    @Override
+    public ContentRemoteRevision readRevision(String revisionId) {
+        Root root = contentRemoteRevisions.get(contentSession.getAuthInfo(), revisionId);
+
+        if (root == null) {
+            return null;
+        }
+
+        return new ContentRemoteRevision(revisionId, root);
+    }
+
+    @Override
+    public ContentRemoteTree readTree(RemoteRevision revision, String path, RemoteTreeFilters filters) {
+        ContentRemoteRevision contentRemoteRevision = null;
+
+        if (revision instanceof ContentRemoteRevision) {
+            contentRemoteRevision = (ContentRemoteRevision) revision;
+        }
+
+        if (contentRemoteRevision == null) {
+            throw new IllegalArgumentException("revision not provided");
+        }
+
+        if (path == null) {
+            throw new IllegalArgumentException("path not provided");
+        }
+
+        if (!isAbsolute(path)) {
+            throw new IllegalArgumentException("invalid path");
+        }
+
+        if (filters == null) {
+            throw new IllegalArgumentException("filters not provided");
+        }
+
+        Root root = contentRemoteRevision.getRoot();
+
+        if (root == null) {
+            throw new IllegalStateException("unable to locate the root");
+        }
+
+        Tree tree = root.getTree(path);
+
+        if (tree.exists()) {
+            return new ContentRemoteTree(tree, 0, filters, contentRemoteBinaries);
+        }
+
+        return null;
+    }
+
+    @Override
+    public ContentRemoteOperation createAddOperation(String path, Map<String, RemoteValue> properties) {
+        if (path == null) {
+            throw new IllegalArgumentException("path not provided");
+        }
+
+        if (!isAbsolute(path)) {
+            throw new IllegalArgumentException("invalid path");
+        }
+
+        if (denotesRoot(path)) {
+            throw new IllegalArgumentException("adding root node");
+        }
+
+        if (properties == null) {
+            throw new IllegalArgumentException("properties not provided");
+        }
+
+        List<ContentRemoteOperation> operations = new ArrayList<ContentRemoteOperation>();
+
+        operations.add(new AddContentRemoteOperation(path));
+
+        for (Map.Entry<String, RemoteValue> entry : properties.entrySet()) {
+            operations.add(createSetOperation(path, entry.getKey(), entry.getValue()));
+        }
+
+        return new AggregateContentRemoteOperation(operations);
+    }
+
+    @Override
+    public ContentRemoteOperation createRemoveOperation(String path) {
+        if (path == null) {
+            throw new IllegalArgumentException("path not provided");
+        }
+
+        if (!isAbsolute(path)) {
+            throw new IllegalArgumentException("invalid path");
+        }
+
+        if (denotesRoot(path)) {
+            throw new IllegalArgumentException("removing root node");
+        }
+
+        return new RemoveContentRemoteOperation(path);
+    }
+
+    @Override
+    public ContentRemoteOperation createSetOperation(String path, String name, RemoteValue value) {
+        if (path == null) {
+            throw new IllegalArgumentException("path not provided");
+        }
+
+        if (!isAbsolute(path)) {
+            throw new IllegalArgumentException("invalid path");
+        }
+
+        if (name == null) {
+            throw new IllegalArgumentException("name not provided");
+        }
+
+        if (name.isEmpty()) {
+            throw new IllegalArgumentException("name is empty");
+        }
+
+        if (value == null) {
+            throw new IllegalArgumentException("value not provided");
+        }
+
+        return new SetContentRemoteOperation(contentRemoteBinaries, path, name, value);
+    }
+
+    @Override
+    public ContentRemoteOperation createUnsetOperation(String path, String name) {
+        if (path == null) {
+            throw new IllegalArgumentException("path not provided");
+        }
+
+        if (!isAbsolute(path)) {
+            throw new IllegalArgumentException("invalid path");
+        }
+
+        if (name == null) {
+            throw new IllegalArgumentException("name not provided");
+        }
+
+        if (name.isEmpty()) {
+            throw new IllegalArgumentException("name is empty");
+        }
+
+        return new UnsetContentRemoteOperation(path, name);
+    }
+
+    @Override
+    public ContentRemoteOperation createCopyOperation(String source, String target) {
+        if (source == null) {
+            throw new IllegalArgumentException("source path not provided");
+        }
+
+        if (!isAbsolute(source)) {
+            throw new IllegalArgumentException("invalid source path");
+        }
+
+        if (target == null) {
+            throw new IllegalArgumentException("target path not provided");
+        }
+
+        if (!isAbsolute(target)) {
+            throw new IllegalArgumentException("invalid target path");
+        }
+
+        if (source.equals(target)) {
+            throw new IllegalArgumentException("same source and target path");
+        }
+
+        if (isAncestor(source, target)) {
+            throw new IllegalArgumentException("source path is an ancestor of target path");
+        }
+
+        return new CopyContentRemoteOperation(source, target);
+    }
+
+    @Override
+    public ContentRemoteOperation createMoveOperation(String source, String target) {
+        if (source == null) {
+            throw new IllegalArgumentException("source path not provided");
+        }
+
+        if (!isAbsolute(source)) {
+            throw new IllegalArgumentException("invalid source path");
+        }
+
+        if (target == null) {
+            throw new IllegalArgumentException("target path not provided");
+        }
+
+        if (!isAbsolute(target)) {
+            throw new IllegalArgumentException("invalid target path");
+        }
+
+        if (source.equals(target)) {
+            throw new IllegalArgumentException("same source and target path");
+        }
+
+        if (isAncestor(source, target)) {
+            throw new IllegalArgumentException("source path is an ancestor of target path");
+        }
+
+        return new MoveContentRemoteOperation(source, target);
+    }
+
+    @Override
+    public ContentRemoteOperation createAggregateOperation(final List<RemoteOperation> operations) {
+        if (operations == null) {
+            throw new IllegalArgumentException("operations not provided");
+        }
+
+        List<ContentRemoteOperation> contentRemoteOperations = new ArrayList<ContentRemoteOperation>();
+
+        for (RemoteOperation operation : operations) {
+            if (operation == null) {
+                throw new IllegalArgumentException("operation not provided");
+            }
+
+            ContentRemoteOperation contentRemoteOperation = null;
+
+            if (operation instanceof ContentRemoteOperation) {
+                contentRemoteOperation = (ContentRemoteOperation) operation;
+            }
+
+            if (contentRemoteOperation == null) {
+                throw new IllegalArgumentException("invalid operation");
+            }
+
+            contentRemoteOperations.add(contentRemoteOperation);
+        }
+
+        return new AggregateContentRemoteOperation(contentRemoteOperations);
+    }
+
+    @Override
+    public ContentRemoteRevision commit(RemoteRevision revision, RemoteOperation operation) throws RemoteCommitException {
+        ContentRemoteRevision contentRemoteRevision = null;
+
+        if (revision instanceof ContentRemoteRevision) {
+            contentRemoteRevision = (ContentRemoteRevision) revision;
+        }
+
+        if (contentRemoteRevision == null) {
+            throw new IllegalArgumentException("invalid revision");
+        }
+
+        ContentRemoteOperation contentRemoteOperation = null;
+
+        if (operation instanceof ContentRemoteOperation) {
+            contentRemoteOperation = (ContentRemoteOperation) operation;
+        }
+
+        if (contentRemoteOperation == null) {
+            throw new IllegalArgumentException("invalid operation");
+        }
+
+        Root root = contentRemoteRevision.getRoot();
+
+        if (root == null) {
+            throw new IllegalStateException("unable to locate the root");
+        }
+
+        contentRemoteOperation.apply(root);
+
+        try {
+            root.commit();
+        } catch (CommitFailedException e) {
+            throw new RemoteCommitException("unable to apply the changes", e);
+        }
+
+        return new ContentRemoteRevision(contentRemoteRevisions.put(contentSession.getAuthInfo(), root), root);
+    }
+
+    @Override
+    public ContentRemoteBinaryId readBinaryId(String binaryId) {
+        if (binaryId == null) {
+            throw new IllegalArgumentException("binary id not provided");
+        }
+
+        if (binaryId.isEmpty()) {
+            throw new IllegalArgumentException("invalid binary id");
+        }
+
+        Blob blob = contentRemoteBinaries.get(binaryId);
+
+        if (blob == null) {
+            return null;
+        }
+
+        return new ContentRemoteBinaryId(binaryId, blob);
+    }
+
+    @Override
+    public InputStream readBinary(RemoteBinaryId binaryId, RemoteBinaryFilters filters) {
+        ContentRemoteBinaryId contentRemoteBinaryId = null;
+
+        if (binaryId instanceof ContentRemoteBinaryId) {
+            contentRemoteBinaryId = (ContentRemoteBinaryId) binaryId;
+        }
+
+        if (contentRemoteBinaryId == null) {
+            throw new IllegalArgumentException("invalid binary id");
+        }
+
+        if (filters == null) {
+            throw new IllegalArgumentException("filters not provided");
+        }
+
+        return new ContentRemoteInputStream(contentRemoteBinaryId.asBlob().getNewStream(), filters);
+    }
+
+    @Override
+    public long readBinaryLength(RemoteBinaryId binaryId) {
+        ContentRemoteBinaryId contentRemoteBinaryId = null;
+
+        if (binaryId instanceof ContentRemoteBinaryId) {
+            contentRemoteBinaryId = (ContentRemoteBinaryId) binaryId;
+        }
+
+        if (contentRemoteBinaryId == null) {
+            throw new IllegalArgumentException("invalid binary id");
+        }
+
+        return contentRemoteBinaryId.asBlob().length();
+    }
+
+    @Override
+    public ContentRemoteBinaryId writeBinary(InputStream stream) {
+        if (stream == null) {
+            throw new IllegalArgumentException("stream not provided");
+        }
+
+        Blob blob;
+
+        try {
+            blob = contentSession.getLatestRoot().createBlob(stream);
+        } catch (IOException e) {
+            throw new RuntimeException("unable to write the binary object", e);
+        }
+
+        return new ContentRemoteBinaryId(contentRemoteBinaries.put(blob), blob);
+    }
+
+    @Override
+    public RemoteResults search(RemoteRevision revision, String query, String language, long offset, long limit) throws RemoteQueryParseException {
+        ContentRemoteRevision contentRemoteRevision = null;
+
+        if (revision instanceof ContentRemoteRevision) {
+            contentRemoteRevision = (ContentRemoteRevision) revision;
+        }
+
+        if (contentRemoteRevision == null) {
+            throw new IllegalArgumentException("invalid revision");
+        }
+
+        Root root = contentRemoteRevision.getRoot();
+
+        if (query == null) {
+            throw new IllegalArgumentException("query not provided");
+        }
+
+        if (language == null) {
+            throw new IllegalArgumentException("language not provided");
+        }
+
+        if (!root.getQueryEngine().getSupportedQueryLanguages().contains(language)) {
+            throw new IllegalArgumentException("language not supported");
+        }
+
+        if (offset < 0) {
+            throw new IllegalArgumentException("invalid offset");
+        }
+
+        if (limit < 0) {
+            throw new IllegalArgumentException("invalid limit");
+        }
+
+        Result results;
+
+        try {
+            results = root.getQueryEngine().executeQuery(query, language, limit, offset, new HashMap<String, PropertyValue>(), new HashMap<String, String>());
+        } catch (ParseException e) {
+            throw new RemoteQueryParseException("invalid query", e);
+        }
+
+        return new ContentRemoteResults(contentRemoteBinaries, results);
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteTree.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteTree.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteTree.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteTree.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,335 @@
+/*
+ * 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.remote.content;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import org.apache.jackrabbit.oak.api.Blob;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.remote.RemoteTree;
+import org.apache.jackrabbit.oak.remote.RemoteTreeFilters;
+import org.apache.jackrabbit.oak.remote.RemoteValue;
+import org.apache.jackrabbit.oak.remote.RemoteValue.Supplier;
+import org.apache.jackrabbit.oak.remote.filter.Filters;
+import org.apache.jackrabbit.util.ISO8601;
+
+import java.io.InputStream;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+
+class ContentRemoteTree implements RemoteTree {
+
+    private final Tree tree;
+
+    private final int depth;
+
+    private final RemoteTreeFilters filters;
+
+    private final ContentRemoteBinaries contentRemoteBinaries;
+
+    public ContentRemoteTree(Tree tree, int depth, RemoteTreeFilters filters, ContentRemoteBinaries contentRemoteBinaries) {
+        this.tree = tree;
+        this.depth = depth;
+        this.filters = filters;
+        this.contentRemoteBinaries = contentRemoteBinaries;
+    }
+
+    @Override
+    public Map<String, RemoteValue> getProperties() {
+        Map<String, RemoteValue> properties = new HashMap<String, RemoteValue>();
+
+        for (PropertyState property : getFilteredProperties()) {
+            properties.put(property.getName(), getRemoteValue(property));
+        }
+
+        return properties;
+    }
+
+    private Iterable<? extends PropertyState> getFilteredProperties() {
+        return Iterables.filter(tree.getProperties(), getPropertyFilters());
+    }
+
+    private Predicate<? super PropertyState> getPropertyFilters() {
+        return new Predicate<PropertyState>() {
+
+            @Override
+            public boolean apply(PropertyState property) {
+                return new Filters(filters.getPropertyFilters()).matches(property.getName());
+            }
+
+        };
+    }
+
+    private RemoteValue getRemoteValue(PropertyState property) {
+        Type<?> type = property.getType();
+
+        if (type == Type.DATE) {
+            return RemoteValue.toDate(getDate(property.getValue(Type.DATE)));
+        }
+
+        if (type == Type.DATES) {
+            return RemoteValue.toMultiDate(getDates(property.getValue(Type.DATES)));
+        }
+
+        if (type == Type.BINARY) {
+            return getBinaryRemoteValue(property.getValue(Type.BINARY));
+        }
+
+        if (type == Type.BINARIES) {
+            return getBinaryRemoteValues(property.getValue(Type.BINARIES));
+        }
+
+        if (type == Type.BOOLEAN) {
+            return RemoteValue.toBoolean(property.getValue(Type.BOOLEAN));
+        }
+
+        if (type == Type.BOOLEANS) {
+            return RemoteValue.toMultiBoolean(property.getValue(Type.BOOLEANS));
+        }
+
+        if (type == Type.DECIMAL) {
+            return RemoteValue.toDecimal(property.getValue(Type.DECIMAL));
+        }
+
+        if (type == Type.DECIMALS) {
+            return RemoteValue.toMultiDecimal(property.getValue(Type.DECIMALS));
+        }
+
+        if (type == Type.DOUBLE) {
+            return RemoteValue.toDouble(property.getValue(Type.DOUBLE));
+        }
+
+        if (type == Type.DOUBLES) {
+            return RemoteValue.toMultiDouble(property.getValue(Type.DOUBLES));
+        }
+
+        if (type == Type.LONG) {
+            return RemoteValue.toLong(property.getValue(Type.LONG));
+        }
+
+        if (type == Type.LONGS) {
+            return RemoteValue.toMultiLong(property.getValue(Type.LONGS));
+        }
+
+        if (type == Type.NAME) {
+            return RemoteValue.toName(property.getValue(Type.NAME));
+        }
+
+        if (type == Type.NAMES) {
+            return RemoteValue.toMultiName(property.getValue(Type.NAMES));
+        }
+
+        if (type == Type.PATH) {
+            return RemoteValue.toPath(property.getValue(Type.PATH));
+        }
+
+        if (type == Type.PATHS) {
+            return RemoteValue.toMultiPath(property.getValue(Type.PATHS));
+        }
+
+        if (type == Type.REFERENCE) {
+            return RemoteValue.toReference(property.getValue(Type.REFERENCE));
+        }
+
+        if (type == Type.REFERENCES) {
+            return RemoteValue.toMultiReference(property.getValue(Type.REFERENCES));
+        }
+
+        if (type == Type.STRING) {
+            return RemoteValue.toText(property.getValue(Type.STRING));
+        }
+
+        if (type == Type.STRINGS) {
+            return RemoteValue.toMultiText(property.getValue(Type.STRINGS));
+        }
+
+        if (type == Type.URI) {
+            return RemoteValue.toUri(property.getValue(Type.URI));
+        }
+
+        if (type == Type.URIS) {
+            return RemoteValue.toMultiUri(property.getValue(Type.URIS));
+        }
+
+        if (type == Type.WEAKREFERENCE) {
+            return RemoteValue.toWeakReference(property.getValue(Type.WEAKREFERENCE));
+        }
+
+        if (type == Type.WEAKREFERENCES) {
+            return RemoteValue.toMultiWeakReference(property.getValue(Type.WEAKREFERENCES));
+        }
+
+        throw new IllegalArgumentException("unrecognized property type");
+    }
+
+    private long getDate(String date) {
+        Calendar calendar = ISO8601.parse(date);
+
+        if (calendar == null) {
+            throw new IllegalStateException("invalid date format");
+        }
+
+        return calendar.getTimeInMillis();
+    }
+
+    private Iterable<Long> getDates(Iterable<String> dates) {
+        return Iterables.transform(dates, new Function<String, Long>() {
+
+            @Override
+            public Long apply(String date) {
+                return getDate(date);
+            }
+
+        });
+    }
+
+    private RemoteValue getBinaryRemoteValue(Blob blob) {
+        if (getLength(blob) < filters.getBinaryThreshold()) {
+            return RemoteValue.toBinary(getBinary(blob));
+        } else {
+            return RemoteValue.toBinaryId(getBinaryId(blob));
+        }
+    }
+
+    private RemoteValue getBinaryRemoteValues(Iterable<Blob> blobs) {
+        if (getLength(blobs) < filters.getBinaryThreshold()) {
+            return RemoteValue.toMultiBinary(getBinaries(blobs));
+        } else {
+            return RemoteValue.toMultiBinaryId(getBinaryIds(blobs));
+        }
+    }
+
+    private long getLength(Blob blob) {
+        return blob.length();
+    }
+
+    private long getLength(Iterable<Blob> blobs) {
+        long length = 0;
+
+        for (Blob blob : blobs) {
+            length = length + blob.length();
+        }
+
+        return length;
+    }
+
+    private Supplier<InputStream> getBinary(final Blob blob) {
+        return new Supplier<InputStream>() {
+
+            @Override
+            public InputStream get() {
+                return blob.getNewStream();
+            }
+
+        };
+    }
+
+    private Iterable<Supplier<InputStream>> getBinaries(Iterable<Blob> blobs) {
+        return Iterables.transform(blobs, new Function<Blob, Supplier<InputStream>>() {
+
+            @Override
+            public Supplier<InputStream> apply(Blob blob) {
+                return getBinary(blob);
+            }
+
+        });
+    }
+
+    private String getBinaryId(Blob blob) {
+        return contentRemoteBinaries.put(blob);
+    }
+
+    private Iterable<String> getBinaryIds(Iterable<Blob> blobs) {
+        return Iterables.transform(blobs, new Function<Blob, String>() {
+
+            @Override
+            public String apply(Blob blob) {
+                return getBinaryId(blob);
+            }
+
+        });
+    }
+
+    @Override
+    public Map<String, RemoteTree> getChildren() {
+        Map<String, RemoteTree> children = new HashMap<String, RemoteTree>();
+
+        for (Tree child : getFilteredChildren()) {
+            if (depth < filters.getDepth()) {
+                children.put(child.getName(), new ContentRemoteTree(child, depth + 1, filters, contentRemoteBinaries));
+            } else {
+                children.put(child.getName(), null);
+            }
+        }
+
+        return children;
+    }
+
+    private Iterable<Tree> getFilteredChildren() {
+        Iterable<Tree> result = tree.getChildren();
+
+        if (filters.getChildrenStart() > 0) {
+            result = Iterables.skip(result, filters.getChildrenStart());
+        }
+
+        if (filters.getChildrenCount() >= 0) {
+            result = Iterables.limit(result, filters.getChildrenCount());
+        }
+
+        return Iterables.filter(result, getNodeFilters());
+    }
+
+    private Predicate<Tree> getNodeFilters() {
+        return new Predicate<Tree>() {
+
+            @Override
+            public boolean apply(Tree child) {
+                return new Filters(filters.getNodeFilters()).matches(child.getName());
+            }
+
+        };
+    }
+
+    @Override
+    public boolean hasMoreChildren() {
+        if (filters.getChildrenCount() < 0) {
+            return false;
+        }
+
+        int start = filters.getChildrenStart();
+
+        if (start < 0) {
+            start = 0;
+        }
+
+        int count = filters.getChildrenCount();
+
+        if (count < 0) {
+            count = 0;
+        }
+
+        int max = start + count;
+
+        return tree.getChildrenCount(max) > max;
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/CopyContentRemoteOperation.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/CopyContentRemoteOperation.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/CopyContentRemoteOperation.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/CopyContentRemoteOperation.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,77 @@
+/*
+ * 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.remote.content;
+
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.remote.RemoteCommitException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class CopyContentRemoteOperation implements ContentRemoteOperation {
+
+    private static final Logger logger = LoggerFactory.getLogger(CopyContentRemoteOperation.class);
+
+    private final String source;
+
+    private final String target;
+
+    public CopyContentRemoteOperation(String source, String target) {
+        this.source = source;
+        this.target = target;
+    }
+
+    @Override
+    public void apply(Root root) throws RemoteCommitException {
+        logger.debug("performing 'copy' operation on source={}, target={}", source, target);
+
+        Tree sourceTree = root.getTree(source);
+
+        if (!sourceTree.exists()) {
+            throw new RemoteCommitException("source tree does not exist");
+        }
+
+        Tree targetTree = root.getTree(target);
+
+        if (targetTree.exists()) {
+            throw new RemoteCommitException("target tree already exists");
+        }
+
+        Tree targetParentTree = targetTree.getParent();
+
+        if (!targetParentTree.exists()) {
+            throw new RemoteCommitException("parent of target tree does not exist");
+        }
+
+        copy(sourceTree, targetParentTree, targetTree.getName());
+    }
+
+    private void copy(Tree source, Tree targetParent, String targetName) {
+        Tree target = targetParent.addChild(targetName);
+
+        for (PropertyState property : source.getProperties()) {
+            target.setProperty(property);
+        }
+
+        for (Tree child : source.getChildren()) {
+            copy(child, target, child.getName());
+        }
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/MoveContentRemoteOperation.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/MoveContentRemoteOperation.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/MoveContentRemoteOperation.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/MoveContentRemoteOperation.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,51 @@
+/*
+ * 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.remote.content;
+
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.remote.RemoteCommitException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class MoveContentRemoteOperation implements ContentRemoteOperation {
+
+    private static final Logger logger = LoggerFactory.getLogger(MoveContentRemoteOperation.class);
+
+    private final String source;
+
+    private final String target;
+
+    public MoveContentRemoteOperation(String source, String target) {
+        this.source = source;
+        this.target = target;
+    }
+
+    @Override
+    public void apply(Root root) throws RemoteCommitException {
+        logger.debug("performing 'move' operation on source={}, target={}", source, target);
+
+        boolean success = root.move(source, target);
+
+        if (success) {
+            return;
+        }
+
+        throw new RemoteCommitException("unable to move the tree");
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/RemoveContentRemoteOperation.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/RemoveContentRemoteOperation.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/RemoveContentRemoteOperation.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/RemoveContentRemoteOperation.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,51 @@
+/*
+ * 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.remote.content;
+
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.remote.RemoteCommitException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class RemoveContentRemoteOperation implements ContentRemoteOperation {
+
+    private static final Logger logger = LoggerFactory.getLogger(RemoveContentRemoteOperation.class);
+
+    private final String path;
+
+    public RemoveContentRemoteOperation(String path) {
+        this.path = path;
+    }
+
+    @Override
+    public void apply(Root root) throws RemoteCommitException {
+        logger.debug("performing 'remove' operation on path={}", path);
+
+        Tree tree = root.getTree(path);
+
+        if (!tree.exists()) {
+            throw new RemoteCommitException("tree does not exists");
+        }
+
+        if (!tree.remove()) {
+            throw new RemoteCommitException("unable to remove the tree");
+        }
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetContentRemoteOperation.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetContentRemoteOperation.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetContentRemoteOperation.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetContentRemoteOperation.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,59 @@
+/*
+ * 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.remote.content;
+
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.remote.RemoteCommitException;
+import org.apache.jackrabbit.oak.remote.RemoteValue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class SetContentRemoteOperation implements ContentRemoteOperation {
+
+    private static final Logger logger = LoggerFactory.getLogger(SetContentRemoteOperation.class);
+
+    private final ContentRemoteBinaries binaries;
+
+    private final String path;
+
+    private final String name;
+
+    private final RemoteValue value;
+
+    public SetContentRemoteOperation(ContentRemoteBinaries binaries, String path, String name, RemoteValue value) {
+        this.binaries = binaries;
+        this.path = path;
+        this.name = name;
+        this.value = value;
+    }
+
+    @Override
+    public void apply(Root root) throws RemoteCommitException {
+        logger.debug("performing 'set' operation on path={}, name={}", path, name);
+
+        Tree tree = root.getTree(path);
+
+        if (!tree.exists()) {
+            throw new RemoteCommitException("tree does not exist");
+        }
+
+        value.whenType(new SetPropertyHandler(binaries, root, tree, name));
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetPropertyHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetPropertyHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetPropertyHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetPropertyHandler.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,242 @@
+/*
+ * 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.remote.content;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import org.apache.jackrabbit.oak.api.Blob;
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.remote.RemoteValue.Supplier;
+import org.apache.jackrabbit.oak.remote.RemoteValue.TypeHandler;
+import org.apache.jackrabbit.util.ISO8601;
+
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.util.Calendar;
+
+class SetPropertyHandler extends TypeHandler {
+
+    private final ContentRemoteBinaries binaries;
+
+    private final Root root;
+
+    private final Tree tree;
+
+    private final String name;
+
+    public SetPropertyHandler(ContentRemoteBinaries binaries, Root root, Tree tree, String name) {
+        this.binaries = binaries;
+        this.root = root;
+        this.tree = tree;
+        this.name = name;
+    }
+
+    @Override
+    public void isBinary(Supplier<InputStream> value) {
+        tree.setProperty(name, getBlob(root, value), Type.BINARY);
+    }
+
+    @Override
+    public void isMultiBinary(Iterable<Supplier<InputStream>> value) {
+        tree.setProperty(name, getBlobs(root, value), Type.BINARIES);
+    }
+
+    @Override
+    public void isBinaryId(String value) {
+        tree.setProperty(name, getBlobFromId(binaries, value), Type.BINARY);
+    }
+
+    @Override
+    public void isMultiBinaryId(Iterable<String> value) {
+        tree.setProperty(name, getBlobsFromIds(binaries, value), Type.BINARIES);
+    }
+
+    @Override
+    public void isBoolean(Boolean value) {
+        tree.setProperty(name, value, Type.BOOLEAN);
+    }
+
+    @Override
+    public void isMultiBoolean(Iterable<Boolean> value) {
+        tree.setProperty(name, value, Type.BOOLEANS);
+    }
+
+    @Override
+    public void isDate(Long value) {
+        tree.setProperty(name, getDate(value), Type.DATE);
+    }
+
+    @Override
+    public void isMultiDate(Iterable<Long> value) {
+        tree.setProperty(name, getDates(value), Type.DATES);
+    }
+
+    @Override
+    public void isDecimal(BigDecimal value) {
+        tree.setProperty(name, value, Type.DECIMAL);
+    }
+
+    @Override
+    public void isMultiDecimal(Iterable<BigDecimal> value) {
+        tree.setProperty(name, value, Type.DECIMALS);
+    }
+
+    @Override
+    public void isDouble(Double value) {
+        tree.setProperty(name, value, Type.DOUBLE);
+    }
+
+    @Override
+    public void isMultiDouble(Iterable<Double> value) {
+        tree.setProperty(name, value, Type.DOUBLES);
+    }
+
+    @Override
+    public void isLong(Long value) {
+        tree.setProperty(name, value, Type.LONG);
+    }
+
+    @Override
+    public void isMultiLong(Iterable<Long> value) {
+        tree.setProperty(name, value, Type.LONGS);
+    }
+
+    @Override
+    public void isName(String value) {
+        tree.setProperty(name, value, Type.NAME);
+    }
+
+    @Override
+    public void isMultiName(Iterable<String> value) {
+        tree.setProperty(name, value, Type.NAMES);
+    }
+
+    @Override
+    public void isPath(String value) {
+        tree.setProperty(name, value, Type.PATH);
+    }
+
+    @Override
+    public void isMultiPath(Iterable<String> value) {
+        tree.setProperty(name, value, Type.PATHS);
+    }
+
+    @Override
+    public void isReference(String value) {
+        tree.setProperty(name, value, Type.REFERENCE);
+    }
+
+    @Override
+    public void isMultiReference(Iterable<String> value) {
+        tree.setProperty(name, value, Type.REFERENCES);
+    }
+
+    @Override
+    public void isText(String value) {
+        tree.setProperty(name, value, Type.STRING);
+    }
+
+    @Override
+    public void isMultiText(Iterable<String> value) {
+        tree.setProperty(name, value, Type.STRINGS);
+    }
+
+    @Override
+    public void isUri(String value) {
+        tree.setProperty(name, value, Type.URI);
+    }
+
+    @Override
+    public void isMultiUri(Iterable<String> value) {
+        tree.setProperty(name, value, Type.URIS);
+    }
+
+    @Override
+    public void isWeakReference(String value) {
+        tree.setProperty(name, value, Type.WEAKREFERENCE);
+    }
+
+    @Override
+    public void isMultiWeakReference(Iterable<String> value) {
+        tree.setProperty(name, value, Type.WEAKREFERENCES);
+    }
+
+    private Blob getBlob(Root root, Supplier<InputStream> supplier) {
+        InputStream inputStream = supplier.get();
+
+        if (inputStream == null) {
+            throw new IllegalStateException("invalid input stream");
+        }
+
+        Blob blob;
+
+        try {
+            blob = root.createBlob(inputStream);
+        } catch (Exception e) {
+            throw new IllegalStateException("unable to create a blob", e);
+        }
+
+        return blob;
+    }
+
+    private Iterable<Blob> getBlobs(final Root root, Iterable<Supplier<InputStream>> suppliers) {
+        return Iterables.transform(suppliers, new Function<Supplier<InputStream>, Blob>() {
+
+            @Override
+            public Blob apply(Supplier<InputStream> supplier) {
+                return getBlob(root, supplier);
+            }
+
+        });
+    }
+
+    private Blob getBlobFromId(ContentRemoteBinaries binaries, String binaryId) {
+        return binaries.get(binaryId);
+    }
+
+    private Iterable<Blob> getBlobsFromIds(final ContentRemoteBinaries binaries, Iterable<String> binaryIds) {
+        return Iterables.transform(binaryIds, new Function<String, Blob>() {
+
+            @Override
+            public Blob apply(String binaryId) {
+                return getBlobFromId(binaries, binaryId);
+            }
+
+        });
+    }
+
+    private String getDate(Long time) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTimeInMillis(time);
+        return ISO8601.format(calendar);
+    }
+
+    private Iterable<String> getDates(Iterable<Long> times) {
+        return Iterables.transform(times, new Function<Long, String>() {
+
+            @Override
+            public String apply(Long time) {
+                return getDate(time);
+            }
+
+        });
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/UnsetContentRemoteOperation.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/UnsetContentRemoteOperation.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/UnsetContentRemoteOperation.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/UnsetContentRemoteOperation.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,56 @@
+/*
+ * 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.remote.content;
+
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.remote.RemoteCommitException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class UnsetContentRemoteOperation implements ContentRemoteOperation {
+
+    private static final Logger logger = LoggerFactory.getLogger(UnsetContentRemoteOperation.class);
+
+    private final String path;
+
+    private final String name;
+
+    public UnsetContentRemoteOperation(String path, String name) {
+        this.path = path;
+        this.name = name;
+    }
+
+    @Override
+    public void apply(Root root) throws RemoteCommitException {
+        logger.debug("performing 'unset' operation on path={}, name={}", path, name);
+
+        Tree tree = root.getTree(path);
+
+        if (!tree.exists()) {
+            throw new RemoteCommitException("tree does not exists");
+        }
+
+        if (!tree.hasProperty(name)) {
+            throw new RemoteCommitException("property does not exist");
+        }
+
+        tree.removeProperty(name);
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filter.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filter.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filter.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filter.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,52 @@
+/*
+ * 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.remote.filter;
+
+import java.util.regex.Pattern;
+
+class Filter {
+
+    private Pattern pattern;
+
+    public Filter(String filter) {
+        StringBuilder builder = new StringBuilder();
+
+        int star = filter.indexOf('*');
+
+        while (star != -1) {
+            if (star > 0 && filter.charAt(star - 1) == '\\') {
+                builder.append(Pattern.quote(filter.substring(0, star - 1)));
+                builder.append(Pattern.quote("*"));
+            } else {
+                builder.append(Pattern.quote(filter.substring(0, star)));
+                builder.append(".*");
+            }
+            filter = filter.substring(star + 1);
+            star = filter.indexOf('*');
+        }
+
+        builder.append(Pattern.quote(filter));
+
+        pattern = Pattern.compile(builder.toString());
+    }
+
+    public boolean matches(String name) {
+        return pattern.matcher(name).matches();
+    }
+
+}
\ No newline at end of file

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filters.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filters.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filters.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filters.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,77 @@
+/*
+ * 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.remote.filter;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class Filters {
+
+    private Set<Filter> includes = new HashSet<Filter>();
+
+    private Set<Filter> excludes = new HashSet<Filter>();
+
+    public Filters(Set<String> filters) {
+        if (filters == null) {
+            throw new IllegalArgumentException("filter set is null");
+        }
+
+        for (String filter : filters) {
+            if (filter == null) {
+                throw new IllegalArgumentException("filter is null");
+            }
+
+            if (filter.length() == 0) {
+                throw new IllegalArgumentException("include filter is an empty string");
+            }
+
+            if (filter.startsWith("-") && filter.length() == 1) {
+                throw new IllegalArgumentException("exclude filter is an empty string");
+            }
+
+        }
+
+        for (String filter : filters) {
+            if (filter.startsWith("-")) {
+                excludes.add(new Filter(filter.substring(1)));
+            } else {
+                includes.add(new Filter(filter));
+            }
+        }
+
+        if (includes.isEmpty()) {
+            includes.add(new Filter("*"));
+        }
+    }
+
+    public boolean matches(String name) {
+        for (Filter include : includes) {
+            if (include.matches(name)) {
+                for (Filter exclude : excludes) {
+                    if (exclude.matches(name)) {
+                        return false;
+                    }
+                }
+
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
\ No newline at end of file

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteHandler.java Thu Jun 11 12:09:15 2015
@@ -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.oak.remote.http;
+
+import org.apache.jackrabbit.oak.remote.http.handler.Handler;
+import org.apache.jackrabbit.oak.remote.http.matcher.Matcher;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+class RemoteHandler implements Matcher, Handler {
+
+    private Matcher matcher;
+
+    private Handler handler;
+
+    public RemoteHandler(Matcher matcher, Handler handler) {
+        this.matcher = matcher;
+        this.handler = handler;
+    }
+
+    @Override
+    public void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+        handler.handle(request, response);
+    }
+
+    @Override
+    public boolean match(HttpServletRequest request) {
+        return matcher.match(request);
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteServlet.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteServlet.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteServlet.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteServlet.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,110 @@
+/*
+ * 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.remote.http;
+
+import org.apache.jackrabbit.oak.remote.RemoteRepository;
+import org.apache.jackrabbit.oak.remote.http.handler.Handler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Arrays;
+
+import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createGetBinaryHandler;
+import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createGetLastRevisionHandler;
+import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createGetLastTreeHandler;
+import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createGetRevisionTreeHandler;
+import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createHeadBinaryHandler;
+import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createHeadLastTreeHandler;
+import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createHeadRevisionTreeHandler;
+import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createNotFoundHandler;
+import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createPatchLastRevisionHandler;
+import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createPatchSpecificRevisionHandler;
+import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createPostBinaryHandler;
+import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createSearchLastRevisionHandler;
+import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createSearchSpecificRevisionHandler;
+import static org.apache.jackrabbit.oak.remote.http.matcher.Matchers.matchesRequest;
+
+public class RemoteServlet extends HttpServlet {
+
+    private static final Logger logger = LoggerFactory.getLogger(RemoteServlet.class);
+
+    private final RemoteRepository repository;
+
+    public RemoteServlet(RemoteRepository repository) {
+        this.repository = repository;
+    }
+
+    @Override
+    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+        request.setAttribute("repository", repository);
+
+        try {
+            firstMatching(readHandlers(), request, createNotFoundHandler()).handle(request, response);
+        } catch (ServletException e) {
+            logger.error("unable to serve the current request", e);
+            throw e;
+        } catch (IOException e) {
+            logger.error("I/O error while serving the current request", e);
+            throw e;
+        } catch (Exception e) {
+            logger.error("unexpected error while serving the current request", e);
+            throw new ServletException(e);
+        }
+    }
+
+    private Handler firstMatching(Iterable<RemoteHandler> handlers, HttpServletRequest request, Handler otherwise) {
+        for (RemoteHandler handler : handlers) {
+            if (handler.match(request)) {
+                return handler;
+            }
+        }
+
+        return otherwise;
+    }
+
+    private Iterable<RemoteHandler> readHandlers() {
+        return handlers(
+                handler("get", "/revisions/last", createGetLastRevisionHandler()),
+                handler("get", "/revisions/last/tree/.*", createGetLastTreeHandler()),
+                handler("head", "/revisions/last/tree/.*", createHeadLastTreeHandler()),
+                handler("get", "/revisions/[^/]+/tree/.*", createGetRevisionTreeHandler()),
+                handler("head", "/revisions/[^/]+/tree/.*", createHeadRevisionTreeHandler()),
+                handler("head", "/binaries/.*", createHeadBinaryHandler()),
+                handler("get", "/binaries/.*", createGetBinaryHandler()),
+                handler("post", "/binaries", createPostBinaryHandler()),
+                handler("patch", "/revisions/last/tree", createPatchLastRevisionHandler()),
+                handler("patch", "/revisions/[^/]+/tree", createPatchSpecificRevisionHandler()),
+                handler("get", "/revisions/last/tree", createSearchLastRevisionHandler()),
+                handler("get", "/revisions/[^/]+/tree", createSearchSpecificRevisionHandler())
+        );
+    }
+
+    private Iterable<RemoteHandler> handlers(RemoteHandler... handlers) {
+        return Arrays.asList(handlers);
+    }
+
+    private RemoteHandler handler(String method, String path, Handler handler) {
+        return new RemoteHandler(matchesRequest(method, path), handler);
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/AuthenticationWrapperHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/AuthenticationWrapperHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/AuthenticationWrapperHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/AuthenticationWrapperHandler.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,182 @@
+/*
+ * 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.remote.http.handler;
+
+import org.apache.jackrabbit.oak.remote.RemoteCredentials;
+import org.apache.jackrabbit.oak.remote.RemoteLoginException;
+import org.apache.jackrabbit.oak.remote.RemoteRepository;
+import org.apache.jackrabbit.oak.remote.RemoteSession;
+import org.apache.jackrabbit.util.Base64;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendInternalServerError;
+
+class AuthenticationWrapperHandler implements Handler {
+
+    private static final Logger logger = LoggerFactory.getLogger(AuthenticationWrapperHandler.class);
+
+    private final Handler authenticated;
+
+    private final Handler notAuthenticated;
+
+    public AuthenticationWrapperHandler(Handler authenticated, Handler notAuthenticated) {
+        this.authenticated = authenticated;
+        this.notAuthenticated = notAuthenticated;
+    }
+
+    @Override
+    public void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+        RemoteSession session = (RemoteSession) request.getAttribute("session");
+
+        if (session != null) {
+            authenticated.handle(request, response);
+            return;
+        }
+
+        RemoteRepository repository = (RemoteRepository) request.getAttribute("repository");
+
+        if (repository == null) {
+            sendInternalServerError(response, "repository not found");
+            return;
+        }
+
+        RemoteCredentials credentials = extractCredentials(request, repository);
+
+        if (credentials == null) {
+            notAuthenticated.handle(request, response);
+            return;
+        }
+
+        try {
+            session = repository.login(credentials);
+        } catch (RemoteLoginException e) {
+            logger.warn("unable to authenticate to the repository", e);
+            notAuthenticated.handle(request, response);
+            return;
+        }
+
+        request.setAttribute("session", session);
+
+        authenticated.handle(request, response);
+    }
+
+    private RemoteCredentials extractCredentials(HttpServletRequest request, RemoteRepository repository) {
+        String authorization = request.getHeader("Authorization");
+
+        if (authorization == null) {
+            return null;
+        }
+
+        String scheme = getScheme(authorization);
+
+        if (!scheme.equalsIgnoreCase("basic")) {
+            return null;
+        }
+
+        String token = getToken(authorization);
+
+        if (token == null) {
+            return null;
+        }
+
+        String decoded;
+
+        try {
+            decoded = Base64.decode(token);
+        } catch (IllegalArgumentException e) {
+            return null;
+        }
+
+        String user = getUser(decoded);
+
+        if (user == null) {
+            return null;
+        }
+
+        String password = getPassword(decoded);
+
+        if (password == null) {
+            return null;
+        }
+
+        return repository.createBasicCredentials(user, password.toCharArray());
+    }
+
+    private String getScheme(String authorization) {
+        int index = authorization.indexOf(' ');
+
+        if (index < 0) {
+            return authorization;
+        }
+
+        return authorization.substring(0, index);
+    }
+
+    private String getToken(String authorization) {
+        int index = authorization.indexOf(' ');
+
+        if (index < 0) {
+            return null;
+        }
+
+        while (index < authorization.length()) {
+            if (authorization.charAt(index) != ' ') {
+                break;
+            }
+
+            index += 1;
+        }
+
+        if (index < authorization.length()) {
+            return authorization.substring(index);
+        }
+
+        return null;
+    }
+
+    private String getUser(String both) {
+        int index = both.indexOf(':');
+
+        if (index < 0) {
+            return null;
+        }
+
+        return both.substring(0, index);
+    }
+
+    private String getPassword(String both) {
+        int index = both.indexOf(':');
+
+        if (index < 0) {
+            return null;
+        }
+
+        if (index + 1 < both.length()) {
+            return both.substring(index + 1);
+        }
+
+        return null;
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetBinaryHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetBinaryHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetBinaryHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetBinaryHandler.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,319 @@
+/*
+ * 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.remote.http.handler;
+
+import com.google.common.io.ByteStreams;
+import org.apache.jackrabbit.oak.remote.RemoteBinaryFilters;
+import org.apache.jackrabbit.oak.remote.RemoteBinaryId;
+import org.apache.jackrabbit.oak.remote.RemoteSession;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.StringTokenizer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendBadRequest;
+import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendInternalServerError;
+import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendNotFound;
+
+class GetBinaryHandler implements Handler {
+
+    private static final String CONTENT_RANGE_HEADER = "Content-Range";
+
+    private static final String RANGE_HEADER = "Range";
+
+    private static final Pattern RANGE_HEADER_PATTERN = Pattern.compile("^\\s*bytes\\s*=\\s*(.*)\\s*$");
+
+    private static final Pattern RANGE_PATTERN = Pattern.compile("^\\s*(\\d*)\\s*(?:\\s*-\\s*(\\d*))?\\s*$");
+
+    private static final String MULTIPART_DELIMITER = "MULTIPART-DELIMITER";
+
+    private static final Pattern REQUEST_PATTERN = Pattern.compile("^/binaries/(.*)$");
+
+    @Override
+    public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
+        RemoteSession session = (RemoteSession) request.getAttribute("session");
+
+        if (session == null) {
+            sendInternalServerError(response, "session not found");
+            return;
+        }
+
+        String providedBinaryId = readBinaryId(request);
+
+        if (providedBinaryId == null) {
+            sendBadRequest(response, "unable to read the provided binary ID");
+            return;
+        }
+
+        RemoteBinaryId binaryId = session.readBinaryId(providedBinaryId);
+
+        if (binaryId == null) {
+            sendNotFound(response, "binary ID not found");
+            return;
+        }
+
+        List<RemoteBinaryFilters> contentRanges = parseRequestRanges(request, session, binaryId);
+
+        if (contentRanges == null) {
+            handleFile(response, session, binaryId);
+        } else if (contentRanges.size() == 1) {
+            handleSingleRange(response, session, binaryId, contentRanges.get(0));
+        } else {
+            handleMultipleRanges(response, session, binaryId, contentRanges);
+        }
+    }
+
+    /**
+     * RFC7233
+     * <p/>
+     * This handler sends a 200 OK http status, the Content-Length header and
+     * the entire file/binary content. This is used when the request Range
+     * header is missing or it contains a malformed value.
+     */
+    private void handleFile(HttpServletResponse response, RemoteSession session, RemoteBinaryId binaryId) throws IOException {
+
+        InputStream in = session.readBinary(binaryId, new RemoteBinaryFilters());
+
+        long length = session.readBinaryLength(binaryId);
+
+        response.setStatus(HttpServletResponse.SC_OK);
+        response.setContentType("application/octet-stream");
+        response.setContentLength((int) length);
+
+        OutputStream out = response.getOutputStream();
+
+        ByteStreams.copy(in, out);
+
+        out.close();
+    }
+
+    /**
+     * RFC7233
+     * <p/>
+     * This handler sends a 206 Partial Content http status, the Content-Length
+     * header, the Content-Range header and the requested binary fragment. This
+     * is used when the request Range header contains only one range.
+     */
+    private void handleSingleRange(HttpServletResponse response, RemoteSession session, RemoteBinaryId binaryId, RemoteBinaryFilters range) throws IOException {
+        InputStream in = session.readBinary(binaryId, range);
+
+        long fileLength = session.readBinaryLength(binaryId);
+        long rangeStart = range.getStart();
+        long rangeEnd = rangeStart + range.getCount() - 1;
+
+        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
+        response.setHeader(CONTENT_RANGE_HEADER, String.format("%d-%d/%d", rangeStart, rangeEnd, fileLength));
+        response.setContentType("application/octet-stream");
+        response.setContentLength((int) (rangeEnd - rangeStart + 1));
+
+        OutputStream out = response.getOutputStream();
+
+        ByteStreams.copy(in, out);
+
+        out.close();
+    }
+
+    /**
+     * RFC7233
+     * <p/>
+     * This handler sends a 206 Partial Content http status, the Content-Length
+     * header, Content-Type multipart/byteranges The payload contains all the
+     * requested binary fragments.
+     * <p/>
+     * This handler is used when multiple ranges are requested.
+     */
+    private void handleMultipleRanges(HttpServletResponse response, RemoteSession session, RemoteBinaryId binaryId, List<RemoteBinaryFilters> ranges) throws IOException {
+
+        String header;
+
+        long rangeStart, rangeEnd, fileLength, contentLength;
+
+        fileLength = session.readBinaryLength(binaryId);
+
+        // Compute response content length
+        // Create multipart headers
+
+        contentLength = 0;
+
+        List<String> multipartHeaders = new ArrayList<String>(ranges.size());
+
+        for (RemoteBinaryFilters range : ranges) {
+            rangeStart = range.getStart();
+            rangeEnd = rangeStart + range.getCount() - 1;
+
+            header = String.format("\n" +
+                            "--%s\n" +
+                            "Content-Type: application/octet-stream" +
+                            "Content-Content-Range: %d-%d/%d\n\n",
+                    MULTIPART_DELIMITER, rangeStart, rangeEnd, fileLength);
+
+            multipartHeaders.add(header);
+
+            contentLength += header.getBytes().length;
+            contentLength += range.getCount();
+        }
+
+        // Send response status and headers
+
+        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
+        response.setContentLength((int) contentLength);
+        response.setContentType("multipart/byteranges; boundary=" + MULTIPART_DELIMITER);
+
+        // Send requested ranges
+
+        RemoteBinaryFilters range;
+
+        InputStream in;
+
+        OutputStream out = response.getOutputStream();
+
+        Iterator<RemoteBinaryFilters> rangeIt = ranges.iterator();
+        Iterator<String> headerIt = multipartHeaders.iterator();
+
+        while (rangeIt.hasNext() && headerIt.hasNext()) {
+            range = rangeIt.next();
+            header = headerIt.next();
+
+            out.write(header.getBytes());
+            in = session.readBinary(binaryId, range);
+            ByteStreams.copy(in, out);
+        }
+
+        out.close();
+    }
+
+    /**
+     * Extract binary id from request path and return it
+     */
+    private String readBinaryId(HttpServletRequest request) {
+        Matcher matcher = REQUEST_PATTERN.matcher(request.getPathInfo());
+
+        if (matcher.matches()) {
+            return matcher.group(1);
+        }
+
+        throw new IllegalStateException("handler bound at the wrong path");
+    }
+
+    /**
+     * This method parses the request Range header a list of ranges as
+     * RemoteBinaryFilters ( or null when the header is missing or contains
+     * invalid/malformed values
+     */
+    private List<RemoteBinaryFilters> parseRequestRanges(HttpServletRequest request, RemoteSession session, RemoteBinaryId binaryId) {
+
+        // Check header exists
+        String headerValue = request.getHeader(RANGE_HEADER);
+
+        if (headerValue == null) {
+            return null;
+        }
+
+        // Check header is bytes=*
+        Matcher matcher = RANGE_HEADER_PATTERN.matcher(headerValue);
+
+        if (!matcher.matches()) {
+            return null;
+        }
+
+        // Iterate requested ranges
+        headerValue = matcher.group(1);
+
+        StringTokenizer tokenizer = new StringTokenizer(headerValue, ",");
+
+        List<RemoteBinaryFilters> ranges = new LinkedList<RemoteBinaryFilters>();
+
+        RemoteBinaryFilters range;
+
+        long fileLength = session.readBinaryLength(binaryId);
+
+        while (tokenizer.hasMoreTokens()) {
+            range = parseRange(tokenizer.nextToken(), fileLength);
+
+            if (range == null) {
+                return null;
+            }
+
+            ranges.add(range);
+        }
+
+        return ranges;
+    }
+
+    /**
+     * Parse a range extracted from the Range header and return a wrapped
+     * RemoteBinaryFilters instance for the range or null if the range is not
+     * valid or malformed.
+     * <p/>
+     * The returned RemoteBinaryFilters object will never return -1 in
+     * getCount.
+     */
+    private RemoteBinaryFilters parseRange(String range, long fileLength) {
+        Matcher matcher = RANGE_PATTERN.matcher(range);
+
+        if (!matcher.matches()) {
+            return null;
+        }
+
+        final long start;
+        final long end;
+
+        // Content-Range: X
+        if (matcher.group(2) == null || matcher.group(2).isEmpty()) {
+            start = Long.parseLong(matcher.group(1));
+            end = fileLength - 1;
+        }
+        // Content-Range: -X
+        else if (matcher.group(1).isEmpty()) {
+            end = fileLength - 1;
+            start = end - Long.parseLong(matcher.group(2)) + 1;
+        }
+        // Content-Range: X-Y
+        else {
+            start = Long.parseLong(matcher.group(1));
+            end = Long.parseLong(matcher.group(2));
+        }
+
+        // Simple range validation
+        if (start < 0 || end < 0 || start > end || end >= fileLength || start >= fileLength) {
+            return null;
+        }
+
+        return new RemoteBinaryFilters() {
+            @Override
+            public long getStart() {
+                return start;
+            }
+
+            @Override
+            public long getCount() {
+                return end - start + 1;
+            }
+        };
+    }
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastRevisionHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastRevisionHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastRevisionHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastRevisionHandler.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,60 @@
+/*
+ * 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.remote.http.handler;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import org.apache.jackrabbit.oak.remote.RemoteRevision;
+import org.apache.jackrabbit.oak.remote.RemoteSession;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendInternalServerError;
+
+class GetLastRevisionHandler implements Handler {
+
+    @Override
+    public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException {
+        RemoteSession session = (RemoteSession) request.getAttribute("session");
+
+        if (session == null) {
+            sendInternalServerError(response, "session not found");
+            return;
+        }
+
+        RemoteRevision revision = session.readLastRevision();
+
+        response.setStatus(HttpServletResponse.SC_OK);
+        response.setContentType("application/json");
+
+        ServletOutputStream stream = response.getOutputStream();
+
+        JsonGenerator generator = new JsonFactory().createJsonGenerator(stream, JsonEncoding.UTF8);
+        generator.writeStartObject();
+        generator.writeStringField("revision", revision.asString());
+        generator.writeEndObject();
+        generator.flush();
+
+        stream.close();
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastTreeHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastTreeHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastTreeHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastTreeHandler.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,46 @@
+/*
+ * 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.remote.http.handler;
+
+import org.apache.jackrabbit.oak.remote.RemoteRevision;
+import org.apache.jackrabbit.oak.remote.RemoteSession;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+class GetLastTreeHandler extends GetTreeHandler {
+
+    private static final Pattern REQUEST_PATTERN = Pattern.compile("^/revisions/last/tree(/.*)$");
+
+    protected String readPath(HttpServletRequest request) {
+        Matcher matcher = REQUEST_PATTERN.matcher(request.getPathInfo());
+
+        if (matcher.matches()) {
+            return matcher.group(1);
+        }
+
+        throw new IllegalStateException("handler bound at the wrong path");
+    }
+
+    @Override
+    protected RemoteRevision readRevision(HttpServletRequest request, RemoteSession session) {
+        return session.readLastRevision();
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetRevisionTreeHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetRevisionTreeHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetRevisionTreeHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetRevisionTreeHandler.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,53 @@
+/*
+ * 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.remote.http.handler;
+
+import org.apache.jackrabbit.oak.remote.RemoteRevision;
+import org.apache.jackrabbit.oak.remote.RemoteSession;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+class GetRevisionTreeHandler extends GetTreeHandler {
+
+    private static final Pattern REQUEST_PATTERN = Pattern.compile("^/revisions/([^/]+)/tree(/.*)$");
+
+    @Override
+    protected String readPath(HttpServletRequest request) {
+        Matcher matcher = REQUEST_PATTERN.matcher(request.getPathInfo());
+
+        if (matcher.matches()) {
+            return matcher.group(2);
+        }
+
+        throw new IllegalStateException("handler bound at the wrong path");
+    }
+
+    @Override
+    protected RemoteRevision readRevision(HttpServletRequest request, RemoteSession session) {
+        Matcher matcher = REQUEST_PATTERN.matcher(request.getPathInfo());
+
+        if (matcher.matches()) {
+            return session.readRevision(matcher.group(1));
+        }
+
+        throw new IllegalStateException("handler bound at the wrong path");
+    }
+
+}