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 [4/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/http/handler/GetTreeHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetTreeHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetTreeHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetTreeHandler.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,264 @@
+/*
+ * 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 com.google.common.collect.Sets;
+import org.apache.jackrabbit.oak.remote.RemoteRevision;
+import org.apache.jackrabbit.oak.remote.RemoteSession;
+import org.apache.jackrabbit.oak.remote.RemoteTree;
+import org.apache.jackrabbit.oak.remote.RemoteTreeFilters;
+import org.apache.jackrabbit.oak.remote.RemoteValue;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+
+import static java.util.Collections.singletonMap;
+import static org.apache.jackrabbit.oak.remote.http.handler.RemoteValues.renderJson;
+import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendGone;
+import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendInternalServerError;
+import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendNotFound;
+
+abstract class GetTreeHandler implements Handler {
+
+    protected abstract String readPath(HttpServletRequest request);
+
+    protected abstract RemoteRevision readRevision(HttpServletRequest request, RemoteSession session);
+
+    @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 = readRevision(request, session);
+
+        if (revision == null) {
+            sendGone(response, "unable to read the revision");
+            return;
+        }
+
+        RemoteTree tree = session.readTree(revision, readPath(request), readFilters(request));
+
+        if (tree == null) {
+            sendNotFound(response, singletonMap("Oak-Revision", revision.asString()), "tree not found");
+            return;
+        }
+
+        response.setStatus(HttpServletResponse.SC_OK);
+        response.setHeader("Oak-Revision", revision.asString());
+        response.setContentType("application/json");
+
+        ServletOutputStream stream = response.getOutputStream();
+
+        JsonGenerator generator = new JsonFactory().createJsonGenerator(stream, JsonEncoding.UTF8);
+        renderResponse(generator, tree);
+        generator.flush();
+
+        stream.close();
+    }
+
+    private void renderResponse(JsonGenerator generator, RemoteTree tree) throws IOException {
+        if (tree == null) {
+            generator.writeNull();
+        } else {
+            generator.writeStartObject();
+            generator.writeFieldName("properties");
+            renderProperties(generator, tree.getProperties());
+            generator.writeFieldName("children");
+            renderChildren(generator, tree.getChildren());
+            generator.writeFieldName("hasMoreChildren");
+            generator.writeBoolean(tree.hasMoreChildren());
+            generator.writeEndObject();
+        }
+    }
+
+    private void renderChildren(JsonGenerator generator, Map<String, RemoteTree> children) throws IOException {
+        generator.writeStartObject();
+
+        for (Map.Entry<String, RemoteTree> entry : children.entrySet()) {
+            generator.writeFieldName(entry.getKey());
+            renderResponse(generator, entry.getValue());
+        }
+
+        generator.writeEndObject();
+    }
+
+    private void renderProperties(JsonGenerator generator, Map<String, RemoteValue> properties) throws IOException {
+        generator.writeStartObject();
+
+        for (Map.Entry<String, RemoteValue> entry : properties.entrySet()) {
+            generator.writeFieldName(entry.getKey());
+            renderJson(generator, entry.getValue());
+        }
+
+        generator.writeEndObject();
+    }
+
+    private RemoteTreeFilters readFilters(final HttpServletRequest request) {
+        return new RemoteTreeFilters() {
+
+            @Override
+            public int getDepth() {
+                Integer depth = readDepth(request);
+
+                if (depth == null) {
+                    return super.getDepth();
+                }
+
+                return depth;
+            }
+
+            @Override
+            public Set<String> getPropertyFilters() {
+                Set<String> propertyFilters = readPropertyFilters(request);
+
+                if (propertyFilters == null) {
+                    return super.getPropertyFilters();
+                }
+
+                return propertyFilters;
+            }
+
+            @Override
+            public Set<String> getNodeFilters() {
+                Set<String> nodeFilters = readNodeFilters(request);
+
+                if (nodeFilters == null) {
+                    return super.getNodeFilters();
+                }
+
+                return nodeFilters;
+            }
+
+            @Override
+            public long getBinaryThreshold() {
+                Long binaryThreshold = readBinaryThreshold(request);
+
+                if (binaryThreshold == null) {
+                    return super.getBinaryThreshold();
+                }
+
+                return binaryThreshold;
+            }
+
+            @Override
+            public int getChildrenStart() {
+                Integer childrenStart = readChildrenStart(request);
+
+                if (childrenStart == null) {
+                    return super.getChildrenStart();
+                }
+
+                return childrenStart;
+            }
+
+            @Override
+            public int getChildrenCount() {
+                Integer childrenCount = readChildrenCount(request);
+
+                if (childrenCount == null) {
+                    return super.getChildrenCount();
+                }
+
+                return childrenCount;
+            }
+
+        };
+    }
+
+    private Integer readDepth(HttpServletRequest request) {
+        return readIntegerParameter(request, "depth");
+    }
+
+    private Set<String> readPropertyFilters(HttpServletRequest request) {
+        return readSetParameter(request, "properties");
+    }
+
+    private Set<String> readNodeFilters(HttpServletRequest request) {
+        return readSetParameter(request, "children");
+    }
+
+    private Long readBinaryThreshold(HttpServletRequest request) {
+        return readLongParameter(request, "binaries");
+    }
+
+    private Integer readChildrenStart(HttpServletRequest request) {
+        return readIntegerParameter(request, "childrenStart");
+    }
+
+    private Integer readChildrenCount(HttpServletRequest request) {
+        return readIntegerParameter(request, "childrenCount");
+    }
+
+    private Integer readIntegerParameter(HttpServletRequest request, String name) {
+        String value = request.getParameter(name);
+
+        if (value == null) {
+            return null;
+        }
+
+        Integer result;
+
+        try {
+            result = Integer.parseInt(value, 10);
+        } catch (NumberFormatException e) {
+            result = null;
+        }
+
+        return result;
+    }
+
+    private Long readLongParameter(HttpServletRequest request, String name) {
+        String value = request.getParameter(name);
+
+        if (value == null) {
+            return null;
+        }
+
+        Long result;
+
+        try {
+            result = Long.parseLong(value, 10);
+        } catch (NumberFormatException e) {
+            result = null;
+        }
+
+        return result;
+    }
+
+    private Set<String> readSetParameter(HttpServletRequest request, String name) {
+        String[] values = request.getParameterValues(name);
+
+        if (values == null) {
+            return null;
+        }
+
+        return Sets.newHashSet(values);
+    }
+
+}

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

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/Handlers.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/Handlers.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/Handlers.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/Handlers.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,157 @@
+/*
+ * 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;
+
+/**
+ * A collection of handlers used to respond to some requests handled by the
+ * remote servlet.
+ */
+public class Handlers {
+
+    private Handlers() {
+    }
+
+    /**
+     * Create an handler that will return the last revision available to the
+     * server.
+     *
+     * @return An instance of {@code Handler}.
+     */
+    public static Handler createGetLastRevisionHandler() {
+        return withAuthentication(new GetLastRevisionHandler());
+    }
+
+    /**
+     * Create an handler that will return a repository sub-tree at a specific
+     * revision.
+     *
+     * @return An instance of {@code Handler}.
+     */
+    public static Handler createGetRevisionTreeHandler() {
+        return withAuthentication(new GetRevisionTreeHandler());
+    }
+
+    /**
+     * Create an handler that will return a repository sub-tree at the latest
+     * known state.
+     *
+     * @return An instance of {@code Handler}.
+     */
+    public static Handler createGetLastTreeHandler() {
+        return withAuthentication(new GetLastTreeHandler());
+    }
+
+    /**
+     * Create an handler that will return a 404 response to the client.
+     *
+     * @return An instance of {@code Handler}.
+     */
+    public static Handler createNotFoundHandler() {
+        return new NotFoundHandler();
+    }
+
+    /**
+     * Create a handler that will read a binary object from the repository.
+     *
+     * @return An instance of {@code Handler}.
+     */
+    public static Handler createGetBinaryHandler() {
+        return withAuthentication(new GetBinaryHandler());
+    }
+
+    /**
+     * Create a handler that will check if a binary exists
+     *
+     * @return An instance of {@code Handler}
+     */
+    public static Handler createHeadBinaryHandler() {
+        return withAuthentication(new HeadBinaryHandler());
+    }
+
+    /**
+     * Create a handler that will perform the creation of new binary object.
+     *
+     * @return An instance of {@code Handler}.
+     */
+    public static Handler createPostBinaryHandler() {
+        return withAuthentication(new PostBinaryHandler());
+    }
+
+    /**
+     * Create a handler that will patch the content at a specific revision.
+     *
+     * @return An instance of {@code Handler}.
+     */
+    public static Handler createPatchSpecificRevisionHandler() {
+        return withAuthentication(new PatchSpecificRevisionHandler());
+    }
+
+    /**
+     * Create a handler that will patch the content at the last revision.
+     *
+     * @return An instance of {@code Handler}.
+     */
+    public static Handler createPatchLastRevisionHandler() {
+        return withAuthentication(new PatchLastRevisionHandler());
+    }
+
+    /**
+     * Create a handler that checks if a tree exists at the last revision.
+     *
+     * @return An instance of {@code Handler}.
+     */
+    public static Handler createHeadLastTreeHandler() {
+        return withAuthentication(new HeadLastTreeHandler());
+    }
+
+    /**
+     * Create a handler that checks if a tree exists at a given revision.
+     *
+     * @return An instance of {@code Handler}.
+     */
+    public static Handler createHeadRevisionTreeHandler() {
+        return withAuthentication(new HeadRevisionTreeHandler());
+    }
+
+    /**
+     * Create a handler that searches for content at the last revision.
+     *
+     * @return An instance of {@code Handler}.
+     */
+    public static Handler createSearchLastRevisionHandler() {
+        return withAuthentication(new SearchLastRevisionHandler());
+    }
+
+    /**
+     * Create a handler that searches for content at the provided revision.
+     *
+     * @return An instance of {@code Handler}.
+     */
+    public static Handler createSearchSpecificRevisionHandler() {
+        return withAuthentication(new SearchSpecificRevisionHandler());
+    }
+
+    private static Handler withAuthentication(Handler authenticated) {
+        return new AuthenticationWrapperHandler(authenticated, createForbiddenHandler());
+    }
+
+    private static Handler createForbiddenHandler() {
+        return new UnauthorizedHandler();
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadBinaryHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadBinaryHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadBinaryHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadBinaryHandler.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.http.handler;
+
+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.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 HeadBinaryHandler implements Handler {
+
+    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;
+        }
+
+        response.setStatus(HttpServletResponse.SC_OK);
+        response.setHeader("Accept-Ranges", "bytes");
+    }
+
+    /**
+     * 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");
+    }
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadLastTreeHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadLastTreeHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadLastTreeHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadLastTreeHandler.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 HeadLastTreeHandler extends HeadTreeHandler {
+
+    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/HeadRevisionTreeHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadRevisionTreeHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadRevisionTreeHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadRevisionTreeHandler.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 HeadRevisionTreeHandler extends HeadTreeHandler {
+
+    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");
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadTreeHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadTreeHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadTreeHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadTreeHandler.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,68 @@
+/*
+ * 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 org.apache.jackrabbit.oak.remote.RemoteTree;
+import org.apache.jackrabbit.oak.remote.RemoteTreeFilters;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+import static java.util.Collections.singletonMap;
+import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendGone;
+import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendInternalServerError;
+import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendNotFound;
+
+abstract class HeadTreeHandler implements Handler {
+
+    protected abstract String readPath(HttpServletRequest request);
+
+    protected abstract RemoteRevision readRevision(HttpServletRequest request, RemoteSession session);
+
+    @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 = readRevision(request, session);
+
+        if (revision == null) {
+            sendGone(response, "revision not found");
+            return;
+        }
+
+        RemoteTree tree = session.readTree(revision, readPath(request), new RemoteTreeFilters());
+
+        if (tree == null) {
+            sendNotFound(response, singletonMap("Oak-Revision", revision.asString()), "tree not found");
+            return;
+        }
+
+        response.setStatus(HttpServletResponse.SC_OK);
+        response.setHeader("Oak-Revision", revision.asString());
+        response.setContentType("application/json");
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/NotFoundHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/NotFoundHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/NotFoundHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/NotFoundHandler.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,33 @@
+/*
+ * 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 javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendNotFound;
+
+class NotFoundHandler implements Handler {
+
+    @Override
+    public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException {
+        sendNotFound(response, "requested path not found");
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchLastRevisionHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchLastRevisionHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchLastRevisionHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchLastRevisionHandler.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+class PatchLastRevisionHandler extends PatchRevisionHandler {
+
+    @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/PatchRevisionHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchRevisionHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchRevisionHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchRevisionHandler.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,445 @@
+/*
+ * 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 com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.jackrabbit.oak.remote.RemoteCommitException;
+import org.apache.jackrabbit.oak.remote.RemoteOperation;
+import org.apache.jackrabbit.oak.remote.RemoteRevision;
+import org.apache.jackrabbit.oak.remote.RemoteSession;
+import org.apache.jackrabbit.oak.remote.RemoteValue;
+import org.apache.jackrabbit.oak.remote.RemoteValue.Supplier;
+import org.apache.jackrabbit.util.Base64;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendBadRequest;
+import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendGone;
+import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendInternalServerError;
+
+abstract class PatchRevisionHandler implements Handler {
+
+    private static final Logger logger = LoggerFactory.getLogger(PatchRevisionHandler.class);
+
+    protected abstract RemoteRevision readRevision(HttpServletRequest request, RemoteSession session);
+
+    @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 base = readRevision(request, session);
+
+        if (base == null) {
+            sendGone(response, "revision not found");
+            return;
+        }
+
+        RemoteOperation operation;
+
+        try {
+            operation = parseOperations(session, new ObjectMapper().readTree(request.getInputStream()));
+        } catch (Exception e) {
+            operation = null;
+        }
+
+        if (operation == null) {
+            sendBadRequest(response, "unable to parse the list of operations");
+            return;
+        }
+
+        RemoteRevision revision;
+
+        try {
+            revision = session.commit(base, operation);
+        } catch (RemoteCommitException e) {
+            logger.warn("unable to perform the commit", e);
+            sendBadRequest(response, "commit failed");
+            return;
+        }
+
+        response.setStatus(HttpServletResponse.SC_CREATED);
+        response.setContentType("application/json");
+
+        ServletOutputStream stream = response.getOutputStream();
+
+        JsonGenerator generator = new JsonFactory().createJsonGenerator(stream, JsonEncoding.UTF8);
+        renderResponse(generator, revision);
+        generator.flush();
+
+        stream.close();
+    }
+
+    private void renderResponse(JsonGenerator generator, RemoteRevision revision) throws IOException {
+        generator.writeStartObject();
+        generator.writeStringField("revision", revision.asString());
+        generator.writeEndObject();
+    }
+
+    private RemoteOperation parseOperations(RemoteSession session, JsonNode json) {
+        List<RemoteOperation> operations = new ArrayList<RemoteOperation>();
+
+        for (JsonNode child : json) {
+            operations.add(parseOperation(session, child));
+        }
+
+        return session.createAggregateOperation(operations);
+    }
+
+    private RemoteOperation parseOperation(RemoteSession session, JsonNode json) {
+        String type = parseStringField(json, "op");
+
+        if (type.equals("add")) {
+            return parseAddOperation(session, json);
+        }
+
+        if (type.equals("remove")) {
+            return parseRemoveOperation(session, json);
+        }
+
+        if (type.equals("set")) {
+            return parseSetOperation(session, json);
+        }
+
+        if (type.equals("unset")) {
+            return parseUnsetOperation(session, json);
+        }
+
+        if (type.equals("copy")) {
+            return parseCopyOperation(session, json);
+        }
+
+        if (type.equals("move")) {
+            return parseMoveOperation(session, json);
+        }
+
+        throw new IllegalArgumentException("invalid operation type");
+    }
+
+    private RemoteOperation parseMoveOperation(RemoteSession session, JsonNode node) {
+        return session.createMoveOperation(parseStringField(node, "from"), parseStringField(node, "to"));
+    }
+
+    private RemoteOperation parseCopyOperation(RemoteSession session, JsonNode node) {
+        return session.createCopyOperation(parseStringField(node, "from"), parseStringField(node, "to"));
+    }
+
+    private RemoteOperation parseUnsetOperation(RemoteSession session, JsonNode node) {
+        return session.createUnsetOperation(parseStringField(node, "path"), parseStringField(node, "name"));
+    }
+
+    private RemoteOperation parseSetOperation(RemoteSession session, JsonNode node) {
+        return session.createSetOperation(parseStringField(node, "path"), parseStringField(node, "name"), parseValue(node));
+    }
+
+    private RemoteOperation parseRemoveOperation(RemoteSession session, JsonNode node) {
+        return session.createRemoveOperation(parseStringField(node, "path"));
+    }
+
+    private RemoteOperation parseAddOperation(RemoteSession session, JsonNode json) {
+        return session.createAddOperation(parseStringField(json, "path"), parsePropertiesField(json, "properties"));
+    }
+
+    private Map<String, RemoteValue> parsePropertiesField(JsonNode node, String name) {
+        return parseProperties(node.get(name));
+    }
+
+    private Map<String, RemoteValue> parseProperties(JsonNode node) {
+        Map<String, RemoteValue> values = new HashMap<String, RemoteValue>();
+
+        Iterator<Map.Entry<String, JsonNode>> iterator = node.fields();
+
+        while (iterator.hasNext()) {
+            Map.Entry<String, JsonNode> entry = iterator.next();
+            values.put(entry.getKey(), parseValue(entry.getValue()));
+        }
+
+        return values;
+    }
+
+    private RemoteValue parseValue(JsonNode node) {
+        String type = parseStringField(node, "type");
+
+        if (type.equals("string")) {
+            return RemoteValue.toText(parseStringField(node, "value"));
+        }
+
+        if (type.equals("strings")) {
+            return RemoteValue.toMultiText(parseStringArrayField(node, "value"));
+        }
+
+        if (type.equals("binary")) {
+            return RemoteValue.toBinary(parseBinaryField(node, "value"));
+        }
+
+        if (type.equals("binaries")) {
+            return RemoteValue.toMultiBinary(parseBinaryArrayField(node, "value"));
+        }
+
+        if (type.equals("binaryId")) {
+            return RemoteValue.toBinaryId(parseStringField(node, "value"));
+        }
+
+        if (type.equals("binaryIds")) {
+            return RemoteValue.toMultiBinaryId(parseStringArrayField(node, "value"));
+        }
+
+        if (type.equals("long")) {
+            return RemoteValue.toLong(parseLongField(node, "value"));
+        }
+
+        if (type.equals("longs")) {
+            return RemoteValue.toMultiLong(parseLongArrayField(node, "value"));
+        }
+
+        if (type.equals("double")) {
+            return RemoteValue.toDouble(parseDoubleField(node, "value"));
+        }
+
+        if (type.equals("doubles")) {
+            return RemoteValue.toMultiDouble(parseDoubleArrayField(node, "value"));
+        }
+
+        if (type.equals("date")) {
+            return RemoteValue.toDate(parseLongField(node, "value"));
+        }
+
+        if (type.equals("dates")) {
+            return RemoteValue.toMultiDate(parseLongArrayField(node, "value"));
+        }
+
+        if (type.equals("boolean")) {
+            return RemoteValue.toBoolean(parseBooleanField(node, "value"));
+        }
+
+        if (type.equals("booleans")) {
+            return RemoteValue.toMultiBoolean(parseBooleanArrayField(node, "value"));
+        }
+
+        if (type.equals("name")) {
+            return RemoteValue.toName(parseStringField(node, "value"));
+        }
+
+        if (type.equals("names")) {
+            return RemoteValue.toMultiName(parseStringArrayField(node, "value"));
+        }
+
+        if (type.equals("path")) {
+            return RemoteValue.toPath(parseStringField(node, "value"));
+        }
+
+        if (type.equals("paths")) {
+            return RemoteValue.toMultiPath(parseStringArrayField(node, "value"));
+        }
+
+        if (type.equals("reference")) {
+            return RemoteValue.toReference(parseStringField(node, "value"));
+        }
+
+        if (type.equals("references")) {
+            return RemoteValue.toMultiReference(parseStringArrayField(node, "value"));
+        }
+
+        if (type.equals("weakReference")) {
+            return RemoteValue.toWeakReference(parseStringField(node, "value"));
+        }
+
+        if (type.equals("weakReferences")) {
+            return RemoteValue.toMultiWeakReference(parseStringArrayField(node, "value"));
+        }
+
+        if (type.equals("uri")) {
+            return RemoteValue.toUri(parseStringField(node, "value"));
+        }
+
+        if (type.equals("uris")) {
+            return RemoteValue.toMultiUri(parseStringArrayField(node, "value"));
+        }
+
+        if (type.equals("decimal")) {
+            return RemoteValue.toDecimal(parseDecimalField(node, "value"));
+        }
+
+        if (type.equals("decimals")) {
+            return RemoteValue.toMultiDecimal(parseDecimalArrayField(node, "value"));
+        }
+
+        throw new IllegalArgumentException("invalid value type");
+    }
+
+    private BigDecimal parseDecimalField(JsonNode node, String field) {
+        return parseDecimal(node.get(field));
+    }
+
+    private BigDecimal parseDecimal(JsonNode node) {
+        return new BigDecimal(node.asText());
+    }
+
+    private Iterable<BigDecimal> parseDecimalArrayField(JsonNode node, String field) {
+        return parseDecimalArray(node.get(field));
+    }
+
+    private Iterable<BigDecimal> parseDecimalArray(JsonNode node) {
+        List<BigDecimal> result = new ArrayList<BigDecimal>();
+
+        for (JsonNode element : node) {
+            result.add(parseDecimal(element));
+        }
+
+        return result;
+    }
+
+    private boolean parseBooleanField(JsonNode node, String field) {
+        return parseBoolean(node.get(field));
+    }
+
+    private boolean parseBoolean(JsonNode node) {
+        return node.asBoolean();
+    }
+
+    private Iterable<Boolean> parseBooleanArrayField(JsonNode node, String field) {
+        return parseBooleanArray(node.get(field));
+    }
+
+    private Iterable<Boolean> parseBooleanArray(JsonNode node) {
+        List<Boolean> result = new ArrayList<Boolean>();
+
+        for (JsonNode element : node) {
+            result.add(parseBoolean(element));
+        }
+
+        return result;
+    }
+
+    private double parseDoubleField(JsonNode node, String field) {
+        return parseDouble(node.get(field));
+    }
+
+    private double parseDouble(JsonNode node) {
+        return node.asDouble();
+    }
+
+    private Iterable<Double> parseDoubleArrayField(JsonNode node, String field) {
+        return parseDoubleArray(node.get(field));
+    }
+
+    private Iterable<Double> parseDoubleArray(JsonNode node) {
+        List<Double> result = new ArrayList<Double>();
+
+        for (JsonNode element : node) {
+            result.add(parseDouble(element));
+        }
+
+        return result;
+    }
+
+    private long parseLongField(JsonNode node, String field) {
+        return parseLong(node.get(field));
+    }
+
+    private long parseLong(JsonNode node) {
+        return node.asLong();
+    }
+
+    private Iterable<Long> parseLongArrayField(JsonNode node, String field) {
+        return parseLongArray(node.get(field));
+    }
+
+    private Iterable<Long> parseLongArray(JsonNode node) {
+        List<Long> result = new ArrayList<Long>();
+
+        for (JsonNode element : node) {
+            result.add(parseLong(element));
+        }
+
+        return result;
+    }
+
+    private Supplier<InputStream> parseBinaryField(JsonNode node, String field) {
+        return parseBinary(node.get(field));
+    }
+
+    private Supplier<InputStream> parseBinary(final JsonNode node) {
+        return new Supplier<InputStream>() {
+
+            @Override
+            public InputStream get() {
+                return new ByteArrayInputStream(Base64.decode(node.asText()).getBytes());
+            }
+
+        };
+    }
+
+    private Iterable<Supplier<InputStream>> parseBinaryArrayField(JsonNode node, String field) {
+        return parseBinaryArray(node.get(field));
+    }
+
+    private Iterable<Supplier<InputStream>> parseBinaryArray(JsonNode node) {
+        List<Supplier<InputStream>> result = new ArrayList<Supplier<InputStream>>();
+
+        for (JsonNode element : node) {
+            result.add(parseBinary(element));
+        }
+
+        return result;
+    }
+
+    private Iterable<String> parseStringArrayField(JsonNode node, String field) {
+        return parseStringArray(node.get(field));
+    }
+
+    private String parseStringField(JsonNode node, String field) {
+        return parseString(node.get(field));
+    }
+
+    private Iterable<String> parseStringArray(JsonNode node) {
+        List<String> result = new ArrayList<String>();
+
+        for (JsonNode element : node) {
+            result.add(parseString(element));
+        }
+
+        return result;
+    }
+
+    private String parseString(JsonNode node) {
+        return node.asText();
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchSpecificRevisionHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchSpecificRevisionHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchSpecificRevisionHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchSpecificRevisionHandler.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,42 @@
+/*
+ * 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 PatchSpecificRevisionHandler extends PatchRevisionHandler {
+
+    private static final Pattern REQUEST_PATTERN = Pattern.compile("^/revisions/([^/]+)/tree$");
+
+    @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");
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PostBinaryHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PostBinaryHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PostBinaryHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PostBinaryHandler.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.RemoteBinaryId;
+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 PostBinaryHandler 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;
+        }
+
+        RemoteBinaryId binaryId = session.writeBinary(request.getInputStream());
+
+        response.setStatus(HttpServletResponse.SC_CREATED);
+        response.setContentType("application/json");
+
+        ServletOutputStream stream = response.getOutputStream();
+
+        JsonGenerator generator = new JsonFactory().createJsonGenerator(stream, JsonEncoding.UTF8);
+        generator.writeStartObject();
+        generator.writeStringField("binaryId", binaryId.asString());
+        generator.writeEndObject();
+        generator.flush();
+
+        stream.close();
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/RemoteValues.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/RemoteValues.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/RemoteValues.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/RemoteValues.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,245 @@
+/*
+ * 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.JsonGenerator;
+import com.google.common.io.BaseEncoding;
+import com.google.common.io.ByteStreams;
+import org.apache.jackrabbit.oak.remote.RemoteValue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigDecimal;
+
+class RemoteValues {
+
+    private RemoteValues() {
+        // Prevent instantiation
+    }
+
+    public static void renderJsonOrNull(JsonGenerator generator, RemoteValue value) throws IOException {
+        if (value == null) {
+            generator.writeNull();
+        } else {
+            renderJson(generator, value);
+        }
+    }
+
+    public static void renderJson(JsonGenerator generator, RemoteValue value) throws IOException {
+        if (value.isBinary()) {
+            renderValue(generator, "binary", value.asBinary(), getBinaryWriter());
+        }
+
+        if (value.isMultiBinary()) {
+            renderMultiValue(generator, "binaries", value.asMultiBinary(), getBinaryWriter());
+        }
+
+        if (value.isBinaryId()) {
+            renderValue(generator, "binaryId", value.asBinaryId(), getStringWriter());
+        }
+
+        if (value.isMultiBinaryId()) {
+            renderMultiValue(generator, "binaryIds", value.asMultiBinaryId(), getStringWriter());
+        }
+
+        if (value.isBoolean()) {
+            renderValue(generator, "boolean", value.asBoolean(), getBooleanWriter());
+        }
+
+        if (value.isMultiBoolean()) {
+            renderMultiValue(generator, "booleans", value.asMultiBoolean(), getBooleanWriter());
+        }
+
+        if (value.isDate()) {
+            renderValue(generator, "date", value.asDate(), getLongWriter());
+        }
+
+        if (value.isMultiDate()) {
+            renderMultiValue(generator, "dates", value.asMultiDate(), getLongWriter());
+        }
+
+        if (value.isDecimal()) {
+            renderValue(generator, "decimal", value.asDecimal(), getDecimalWriter());
+        }
+
+        if (value.isMultiDecimal()) {
+            renderMultiValue(generator, "decimals", value.asMultiDecimal(), getDecimalWriter());
+        }
+
+        if (value.isDouble()) {
+            renderValue(generator, "double", value.asDouble(), getDoubleWriter());
+        }
+
+        if (value.isMultiDouble()) {
+            renderMultiValue(generator, "doubles", value.asMultiDouble(), getDoubleWriter());
+        }
+
+        if (value.isLong()) {
+            renderValue(generator, "long", value.asLong(), getLongWriter());
+        }
+
+        if (value.isMultiLong()) {
+            renderMultiValue(generator, "longs", value.asMultiLong(), getLongWriter());
+        }
+
+        if (value.isName()) {
+            renderValue(generator, "name", value.asName(), getStringWriter());
+        }
+
+        if (value.isMultiName()) {
+            renderMultiValue(generator, "names", value.asMultiName(), getStringWriter());
+        }
+
+        if (value.isPath()) {
+            renderValue(generator, "path", value.asPath(), getStringWriter());
+        }
+
+        if (value.isMultiPath()) {
+            renderMultiValue(generator, "paths", value.asMultiPath(), getStringWriter());
+        }
+
+        if (value.isReference()) {
+            renderValue(generator, "reference", value.asReference(), getStringWriter());
+        }
+
+        if (value.isMultiReference()) {
+            renderMultiValue(generator, "references", value.asMultiReference(), getStringWriter());
+        }
+
+        if (value.isText()) {
+            renderValue(generator, "string", value.asText(), getStringWriter());
+        }
+
+        if (value.isMultiText()) {
+            renderMultiValue(generator, "strings", value.asMultiText(), getStringWriter());
+        }
+
+        if (value.isUri()) {
+            renderValue(generator, "uri", value.asUri(), getStringWriter());
+        }
+
+        if (value.isMultiUri()) {
+            renderMultiValue(generator, "uris", value.asMultiUri(), getStringWriter());
+        }
+
+        if (value.isWeakReference()) {
+            renderValue(generator, "weakReference", value.asWeakReference(), getStringWriter());
+        }
+
+        if (value.isMultiWeakReference()) {
+            renderMultiValue(generator, "weakReferences", value.asMultiWeakReference(), getStringWriter());
+        }
+    }
+
+    private static GeneratorWriter<RemoteValue.Supplier<InputStream>> getBinaryWriter() {
+        return new GeneratorWriter<RemoteValue.Supplier<InputStream>>() {
+
+            @Override
+            public void write(JsonGenerator generator, RemoteValue.Supplier<InputStream> value) throws IOException {
+                generator.writeString(BaseEncoding.base64().encode(ByteStreams.toByteArray(value.get())));
+            }
+
+        };
+    }
+
+    private static GeneratorWriter<String> getStringWriter() {
+        return new GeneratorWriter<String>() {
+
+            @Override
+            public void write(JsonGenerator generator, String value) throws IOException {
+                generator.writeString(value);
+            }
+
+        };
+    }
+
+    private static GeneratorWriter<Boolean> getBooleanWriter() {
+        return new GeneratorWriter<Boolean>() {
+
+            @Override
+            public void write(JsonGenerator generator, Boolean value) throws IOException {
+                generator.writeBoolean(value);
+            }
+
+        };
+    }
+
+    private static GeneratorWriter<Long> getLongWriter() {
+        return new GeneratorWriter<Long>() {
+
+            @Override
+            public void write(JsonGenerator generator, Long value) throws IOException {
+                generator.writeNumber(value);
+            }
+
+        };
+    }
+
+    private static GeneratorWriter<BigDecimal> getDecimalWriter() {
+        return new GeneratorWriter<BigDecimal>() {
+
+            @Override
+            public void write(JsonGenerator generator, BigDecimal value) throws IOException {
+                generator.writeString(value.toString());
+            }
+
+        };
+    }
+
+    private static GeneratorWriter<Double> getDoubleWriter() {
+        return new GeneratorWriter<Double>() {
+
+            @Override
+            public void write(JsonGenerator generator, Double value) throws IOException {
+                generator.writeNumber(value);
+            }
+
+        };
+    }
+
+    private static <T> void renderValue(JsonGenerator generator, String type, T value, GeneratorWriter<T> writer) throws IOException {
+        generator.writeStartObject();
+        generator.writeStringField("type", type);
+        generator.writeFieldName("value");
+
+        writer.write(generator, value);
+
+        generator.writeEndObject();
+    }
+
+    private static <T> void renderMultiValue(JsonGenerator generator, String type, Iterable<T> values, GeneratorWriter<T> writer) throws IOException {
+        generator.writeStartObject();
+        generator.writeStringField("type", type);
+        generator.writeArrayFieldStart("value");
+
+        for (T value : values) {
+            writer.write(generator, value);
+        }
+
+        generator.writeEndArray();
+        generator.writeEndObject();
+    }
+
+
+    private interface GeneratorWriter<T> {
+
+        void write(JsonGenerator generator, T value) throws IOException;
+
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/ResponseUtils.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/ResponseUtils.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/ResponseUtils.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/ResponseUtils.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,87 @@
+/*
+ * 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 javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+class ResponseUtils {
+
+    private ResponseUtils() {
+    }
+
+    private static void send(HttpServletResponse response, int code, String message) throws IOException {
+        send(response, code, new HashMap<String, String>(), message);
+    }
+
+    private static void send(HttpServletResponse response, int code, Map<String, String> headers, String message) throws IOException {
+        response.setStatus(code);
+        response.setContentType("application/json");
+
+        for (Map.Entry<String, String> entry : headers.entrySet()) {
+            response.setHeader(entry.getKey(), entry.getValue());
+        }
+
+        ServletOutputStream stream = response.getOutputStream();
+
+        JsonGenerator generator = new JsonFactory().createJsonGenerator(stream, JsonEncoding.UTF8);
+        generator.writeStartObject();
+        generator.writeStringField("error", message);
+        generator.writeEndObject();
+        generator.flush();
+
+        stream.close();
+    }
+
+    public static void sendBadRequest(HttpServletResponse response, String message) throws IOException {
+        send(response, HttpServletResponse.SC_BAD_REQUEST, message);
+    }
+
+    public static void sendInternalServerError(HttpServletResponse response, String message) throws IOException {
+        send(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, message);
+    }
+
+    public static void sendGone(HttpServletResponse response, String message) throws IOException {
+        send(response, HttpServletResponse.SC_GONE, message);
+    }
+
+    public static void sendNotFound(HttpServletResponse response, String message) throws IOException {
+        send(response, HttpServletResponse.SC_NOT_FOUND, message);
+    }
+
+    public static void sendNotFound(HttpServletResponse response, Map<String, String> headers, String message) throws IOException {
+        send(response, HttpServletResponse.SC_NOT_FOUND, headers, message);
+    }
+
+    public static void sendUnauthorized(HttpServletResponse response, String message) throws IOException {
+        send(response, HttpServletResponse.SC_UNAUTHORIZED, message);
+    }
+
+    public static void sendUnauthorized(HttpServletResponse response, Map<String, String> headers, String message) throws IOException {
+        send(response, HttpServletResponse.SC_UNAUTHORIZED, headers, message);
+    }
+
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchLastRevisionHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchLastRevisionHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchLastRevisionHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchLastRevisionHandler.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+class SearchLastRevisionHandler extends SearchRevisionHandler {
+
+    @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/SearchRevisionHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchRevisionHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchRevisionHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchRevisionHandler.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,209 @@
+/*
+ * 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.RemoteQueryParseException;
+import org.apache.jackrabbit.oak.remote.RemoteResult;
+import org.apache.jackrabbit.oak.remote.RemoteResults;
+import org.apache.jackrabbit.oak.remote.RemoteRevision;
+import org.apache.jackrabbit.oak.remote.RemoteSession;
+
+import javax.servlet.ServletException;
+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.RemoteValues.renderJson;
+import static org.apache.jackrabbit.oak.remote.http.handler.RemoteValues.renderJsonOrNull;
+import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendBadRequest;
+import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendGone;
+import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendInternalServerError;
+
+abstract class SearchRevisionHandler implements Handler {
+
+    protected abstract RemoteRevision readRevision(HttpServletRequest request, RemoteSession session);
+
+    @Override
+    public void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+        RemoteSession session = (RemoteSession) request.getAttribute("session");
+
+        if (session == null) {
+            sendInternalServerError(response, "session not found");
+            return;
+        }
+
+        RemoteRevision revision = readRevision(request, session);
+
+        if (revision == null) {
+            sendGone(response, "unable to read the revision");
+            return;
+        }
+
+        String query = readQuery(request);
+
+        if (query == null) {
+            sendBadRequest(response, "query not specified");
+            return;
+        }
+
+        String language = readLanguage(request);
+
+        if (language == null) {
+            sendBadRequest(response, "language not specified");
+            return;
+        }
+
+        Long offset = readOffset(request);
+
+        if (offset == null) {
+            sendBadRequest(response, "offset not specified");
+            return;
+        }
+
+        Long limit = readLimit(request);
+
+        if (limit == null) {
+            sendBadRequest(response, "limit not specified");
+            return;
+        }
+
+        RemoteResults results;
+
+        try {
+            results = session.search(revision, query, language, offset, limit);
+        } catch (RemoteQueryParseException e) {
+            sendBadRequest(response, "malformed query");
+            return;
+        }
+
+        response.setStatus(HttpServletResponse.SC_OK);
+        response.setHeader("Oak-Revision", revision.asString());
+        response.setContentType("application/json");
+
+        ServletOutputStream stream = response.getOutputStream();
+
+        JsonGenerator generator = new JsonFactory().createJsonGenerator(stream, JsonEncoding.UTF8);
+        renderResponse(generator, results);
+        generator.flush();
+
+        stream.close();
+    }
+
+    private String readQuery(HttpServletRequest request) {
+        return readStringParameter(request, "query");
+    }
+
+    private String readLanguage(HttpServletRequest request) {
+        return readStringParameter(request, "language");
+    }
+
+    private String readStringParameter(HttpServletRequest request, String name) {
+        return request.getParameter(name);
+    }
+
+    private Long readOffset(HttpServletRequest request) {
+        return readLongParameter(request, "offset");
+    }
+
+    private Long readLimit(HttpServletRequest request) {
+        return readLongParameter(request, "limit");
+    }
+
+    private Long readLongParameter(HttpServletRequest request, String name) {
+        String value = readStringParameter(request, name);
+
+        if (value == null) {
+            return null;
+        }
+
+        try {
+            return Long.parseLong(value, 10);
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    private void renderResponse(JsonGenerator generator, RemoteResults results) throws IOException {
+        generator.writeStartObject();
+        generator.writeFieldName("total");
+        generator.writeNumber(results.getTotal());
+        generator.writeFieldName("columns");
+        renderStrings(generator, results.getColumns());
+        generator.writeFieldName("selectors");
+        renderStrings(generator, results.getSelectors());
+        generator.writeFieldName("results");
+        renderResults(generator, results);
+        generator.writeEndObject();
+    }
+
+    private void renderStrings(JsonGenerator generator, Iterable<String> elements) throws IOException {
+        generator.writeStartArray();
+
+        for (String element : elements) {
+            generator.writeString(element);
+        }
+
+        generator.writeEndArray();
+    }
+
+    private void renderResults(JsonGenerator generator, RemoteResults results) throws IOException {
+        generator.writeStartArray();
+
+        for (RemoteResult result : results) {
+            renderResult(generator, results, result);
+        }
+
+        generator.writeEndArray();
+    }
+
+    private void renderResult(JsonGenerator generator, RemoteResults results, RemoteResult result) throws IOException {
+        generator.writeStartObject();
+        generator.writeFieldName("columns");
+        renderColumns(generator, results, result);
+        generator.writeFieldName("selectors");
+        renderSelectors(generator, results, result);
+        generator.writeEndObject();
+    }
+
+    private void renderColumns(JsonGenerator generator, RemoteResults results, RemoteResult result) throws IOException {
+        generator.writeStartObject();
+
+        for (String name : results.getColumns()) {
+            generator.writeFieldName(name);
+            renderJsonOrNull(generator, result.getColumnValue(name));
+        }
+
+        generator.writeEndObject();
+    }
+
+    private void renderSelectors(JsonGenerator generator, RemoteResults results, RemoteResult result) throws IOException {
+        generator.writeStartObject();
+
+        for (String name : results.getSelectors()) {
+            generator.writeFieldName(name);
+            generator.writeString(result.getSelectorPath(name));
+        }
+
+        generator.writeEndObject();
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchSpecificRevisionHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchSpecificRevisionHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchSpecificRevisionHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchSpecificRevisionHandler.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,42 @@
+/*
+ * 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 SearchSpecificRevisionHandler extends SearchRevisionHandler {
+
+    private static final Pattern REQUEST_PATTERN = Pattern.compile("^/revisions/([^/]+)/tree$");
+
+    @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");
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/UnauthorizedHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/UnauthorizedHandler.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/UnauthorizedHandler.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/UnauthorizedHandler.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,34 @@
+/*
+ * 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 javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Collections;
+
+import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendUnauthorized;
+
+class UnauthorizedHandler implements Handler {
+
+    @Override
+    public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException {
+        sendUnauthorized(response, Collections.singletonMap("WWW-Authenticate", "Basic realm=\"Oak\""), "unable to authenticate");
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/AllMatcher.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/AllMatcher.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/AllMatcher.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/AllMatcher.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,41 @@
+/*
+ * 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.matcher;
+
+import javax.servlet.http.HttpServletRequest;
+
+class AllMatcher implements Matcher {
+
+    private final Matcher[] matchers;
+
+    public AllMatcher(Matcher... matchers) {
+        this.matchers = matchers;
+    }
+
+    @Override
+    public boolean match(HttpServletRequest request) {
+        for (Matcher matcher : matchers) {
+            if (!matcher.match(request)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matcher.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matcher.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matcher.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matcher.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,37 @@
+/*
+ * 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.matcher;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * A predicate over an HTTP request. This predicate can be used to check if some
+ * preconditions on the request are met.
+ */
+public interface Matcher {
+
+    /**
+     * Check if the preconditions on the given request are met.
+     *
+     * @param request Request to check.
+     * @return {@code true} if the preconditions are met, {@code false}
+     * otherwise.
+     */
+    boolean match(HttpServletRequest request);
+
+}

Added: jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matchers.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matchers.java?rev=1684861&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matchers.java (added)
+++ jackrabbit/oak/trunk/oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matchers.java Thu Jun 11 12:09:15 2015
@@ -0,0 +1,100 @@
+/*
+ * 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.matcher;
+
+import java.util.regex.Pattern;
+
+/**
+ * Collection of matchers for HTTP requests.
+ */
+public class Matchers {
+
+    private Matchers() {
+    }
+
+    /**
+     * Create a matcher that will be satisfied when given requests have a method
+     * matching the one provided as a parameter.
+     *
+     * @param method Method that requests must have for the matcher to be
+     *               satisfied.
+     * @return An instance of {@code Matcher}.
+     */
+    public static Matcher matchesMethod(String method) {
+        if (method == null) {
+            throw new IllegalArgumentException("method not provided");
+        }
+
+        return new MethodMatcher(method);
+    }
+
+    /**
+     * Create a matcher that will be satisfied when given requests have a patch
+     * matching the pattern provided as a parameter.
+     *
+     * @param pattern The pattern to use when checking the requests given to the
+     *                matcher.
+     * @return An instance of {@code Matcher}.
+     */
+    public static Matcher matchesPath(String pattern) {
+        if (pattern == null) {
+            throw new IllegalArgumentException("pattern not provided");
+        }
+
+        return new PathMatcher(Pattern.compile(pattern));
+    }
+
+    /**
+     * Create a matcher that will be satisfied when the given requests satisfies
+     * every matcher provided as parameters. Calling this method is equivalent
+     * as checking every provided matcher individually and chaining each result
+     * as a short-circuit and.
+     *
+     * @param matchers The matchers that have to be satisfied for the returned
+     *                 matcher to be satisfied.
+     * @return An instance of {@code Matcher}.
+     */
+    public static Matcher matchesAll(Matcher... matchers) {
+        if (matchers == null) {
+            throw new IllegalArgumentException("matchers not provided");
+        }
+
+        for (Matcher matcher : matchers) {
+            if (matcher == null) {
+                throw new IllegalArgumentException("invalid matcher");
+            }
+        }
+
+        return new AllMatcher(matchers);
+    }
+
+    /**
+     * Create a matcher that will be satisifed when the given requests match the
+     * provided method and path.
+     *
+     * @param method The method that requests must have for the matcher to be
+     *               satisfied.
+     * @param path   The pattern to use when checking the requests given to the
+     *               matcher.
+     * @return An instance of {@code Matcher}.
+     */
+    public static Matcher matchesRequest(String method, String path) {
+        return matchesAll(matchesMethod(method), matchesPath(path));
+    }
+
+}