diff --git oak-remote/pom.xml oak-remote/pom.xml
new file mode 100644
index 0000000..8e112f3
--- /dev/null
+++ oak-remote/pom.xml
@@ -0,0 +1,203 @@
+
+
+
+
+
+ 4.0.0
+
+
+ org.apache.jackrabbit
+ oak-parent
+ 1.4-SNAPSHOT
+ ../oak-parent/pom.xml
+
+
+ oak-remote
+ Oak Remote API
+ bundle
+
+
+ MEMORY_NS,SEGMENT_MK
+
+
+
+
+ org.osgi
+ org.osgi.compendium
+ provided
+
+
+ com.google.guava
+ guava
+ provided
+
+
+ org.slf4j
+ slf4j-api
+ provided
+
+
+ javax.jcr
+ jcr
+ 2.0
+ provided
+
+
+ org.apache.jackrabbit
+ oak-core
+ ${project.version}
+ provided
+
+
+ org.apache.jackrabbit
+ oak-jcr
+ ${project.version}
+ provided
+
+
+ org.apache.jackrabbit
+ oak-commons
+ ${project.version}
+ provided
+
+
+ org.apache.jackrabbit
+ jackrabbit-jcr-commons
+ 2.9.0
+ provided
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ 2.0.0
+ provided
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.0.0
+ provided
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.0.1
+ provided
+
+
+ org.eclipse.jetty
+ jetty-server
+ 8.1.10.v20130312
+ test
+
+
+ org.eclipse.jetty
+ jetty-servlet
+ 8.1.10.v20130312
+ test
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ org.hamcrest
+ hamcrest-core
+ 1.3
+ test
+
+
+ org.mockito
+ mockito-all
+ 1.10.19
+ test
+
+
+ com.mashape.unirest
+ unirest-java
+ 1.3.27
+ test
+
+
+ org.apache.jackrabbit
+ oak-core
+ ${project.version}
+ test-jar
+ test
+
+
+ org.apache.jackrabbit
+ oak-commons
+ ${project.version}
+ test-jar
+ test
+
+
+ org.mongodb
+ mongo-java-driver
+ test
+
+
+ org.slf4j
+ slf4j-nop
+ ${slf4j.version}
+ test
+
+
+ org.apache.felix
+ org.apache.felix.scr.annotations
+ 1.9.10
+ compile
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ 2.18.1
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+
+
+
+
+
+
+
+ org.apache.felix
+ maven-scr-plugin
+
+
+
+
\ No newline at end of file
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteBinaryFilters.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteBinaryFilters.java
new file mode 100644
index 0000000..13bec14
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteBinaryFilters.java
@@ -0,0 +1,43 @@
+/*
+ * 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;
+
+/**
+ * Represents a set of filters that can be applied when a binary object is read
+ * from the repository.
+ */
+public class RemoteBinaryFilters {
+
+ /**
+ * Return the starting offset into the binary object. This method returns
+ * {@code 0} by default, meaning that the binary object should be read from
+ * the beginning.
+ */
+ public long getStart() {
+ return 0;
+ }
+
+ /**
+ * Return the number of bytes to read. This method returns {@code -1} by
+ * default, meaning that the binary object should be read until the end.
+ */
+ public long getCount() {
+ return -1;
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteBinaryId.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteBinaryId.java
new file mode 100644
index 0000000..30f1d6f
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteBinaryId.java
@@ -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;
+
+/**
+ * Represents the identifier of a binary object.
+ *
+ * Binary objects stored in the repository are immutable. The same binary ID is
+ * guaranteed to return the same binary object over time.
+ */
+public interface RemoteBinaryId {
+
+ /**
+ * Returns a string representation of the binary ID. This representation can
+ * be used as a reference by an external system and converted to a {@code
+ * RemoteBinaryId} object using {@link RemoteSession#readBinaryId(String)}.
+ *
+ * @return A string representation of the binary ID.
+ */
+ String asString();
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteCommitException.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteCommitException.java
new file mode 100644
index 0000000..0a0b2d1
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteCommitException.java
@@ -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;
+
+/**
+ * Represents an error condition caused from a set of changes that can't be
+ * committed into the repository.
+ */
+public class RemoteCommitException extends Exception {
+
+ public RemoteCommitException() {
+ }
+
+ public RemoteCommitException(String s) {
+ super(s);
+ }
+
+ public RemoteCommitException(String s, Throwable throwable) {
+ super(s, throwable);
+ }
+
+ public RemoteCommitException(Throwable throwable) {
+ super(throwable);
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteCredentials.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteCredentials.java
new file mode 100644
index 0000000..1eec995
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteCredentials.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/**
+ * This interface is a marker interface to be used to represent both an
+ * authentication strategy and the information to enable that authentication
+ * strategy.
+ *
+ * In example, a {@code RemoteCredentials} object created to represent an
+ * authentication strategy based on user name and password, may encapsulate the
+ * user name and password to enable this authentication strategy when a session
+ * to the repository is created.
+ *
+ * To create instances of this interface, take a look at the methods defined in
+ * {@link RemoteRepository}.
+ */
+public interface RemoteCredentials {
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteLoginException.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteLoginException.java
new file mode 100644
index 0000000..a8d245d
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteLoginException.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+/**
+ * Represents an error condition generated by an unsuccessful login.
+ */
+public class RemoteLoginException extends Exception {
+
+ public RemoteLoginException() {
+ }
+
+ public RemoteLoginException(String s) {
+ super(s);
+ }
+
+ public RemoteLoginException(String s, Throwable throwable) {
+ super(s, throwable);
+ }
+
+ public RemoteLoginException(Throwable throwable) {
+ super(throwable);
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteOperation.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteOperation.java
new file mode 100644
index 0000000..6080c26
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteOperation.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+/**
+ * An operation to modify content inside the repository.
+ *
+ * This is just a marker interface. Factory methods for {@code RemoteOperation}
+ * instances are found in {@link RemoteSession}. The interface doesn't currently
+ * expose any method, as its instances are supposed to be consumed by the same
+ * {@code RemoteSession} (or by another instance of the same implementation)
+ * that created them.
+ */
+public interface RemoteOperation {
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteQueryParseException.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteQueryParseException.java
new file mode 100644
index 0000000..9553c4a
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteQueryParseException.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+/**
+ * An error thrown if a query can't be correctly parsed.
+ */
+public class RemoteQueryParseException extends Exception {
+
+ public RemoteQueryParseException() {
+ }
+
+ public RemoteQueryParseException(String s) {
+ super(s);
+ }
+
+ public RemoteQueryParseException(String s, Throwable throwable) {
+ super(s, throwable);
+ }
+
+ public RemoteQueryParseException(Throwable throwable) {
+ super(throwable);
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteRepository.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteRepository.java
new file mode 100644
index 0000000..d768479
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteRepository.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+import java.util.Set;
+
+/**
+ * The remote interface exposed by a repository.
+ *
+ * Most methods require authentication to be used. As such, a client of this
+ * interface is required to create an instance of {@code RemoteCredentials} and
+ * use this instance to login into the repository and obtain a {@code
+ * RemoteSession} object.
+ */
+public interface RemoteRepository {
+
+ /**
+ * Create a {@code RemoteCredentials} object representing an authentication
+ * strategy based on a user name and a password. This kind of credentials
+ * delegates authentication to the repository, using a user identified by
+ * the user name and password provided to this method.
+ *
+ * @param user User name.
+ * @param password Password.
+ * @return A {@code RemoteCredentials} object representing an authentication
+ * strategy based on a user name and password.
+ */
+ RemoteCredentials createBasicCredentials(String user, char[] password);
+
+ /**
+ * Create a {@code RemoteCredentials} object representing an impersonation
+ * authentication strategy. If this authentication strategy is used, the
+ * repository will not make any attempt to perform authentication. It will
+ * instead trust the information provided by the {@code RemoteCredentials}
+ * and will create a {@code RemoteSession} bound to the principals specified
+ * to this method.
+ *
+ * @param principals The set of principals to impersonate into.
+ * @return A {@code RemoteCredentials} object representing an authentication
+ * strategy based on impersonation.
+ */
+ RemoteCredentials createImpersonationCredentials(Set principals);
+
+ /**
+ * Create a remote session exposing some repository operations.
+ *
+ * @param credentials An object representing an authentication strategy to
+ * be used when invoking repository operations.
+ * @return An instance of a remote session to invoke repository operations.
+ * @throws RemoteLoginException if it was not possible to authenticate the
+ * given credentials.
+ */
+ RemoteSession login(RemoteCredentials credentials) throws RemoteLoginException;
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteResult.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteResult.java
new file mode 100644
index 0000000..0b0442d
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteResult.java
@@ -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;
+
+/**
+ * A single search result.
+ */
+public interface RemoteResult {
+
+ /**
+ * The value attached to this search result at the specified column.
+ *
+ * @param column Name of the column.
+ * @return An instance of {@code RemoteValue}.
+ */
+ RemoteValue getColumnValue(String column);
+
+ /**
+ * Read the path of the node associated with the specified selector.
+ *
+ * @param selector The name of the selector.
+ * @return A node path.
+ */
+ String getSelectorPath(String selector);
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteResults.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteResults.java
new file mode 100644
index 0000000..4914d90
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteResults.java
@@ -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;
+
+/**
+ * A collection of search results.
+ */
+public interface RemoteResults extends Iterable {
+
+ /**
+ * If available, it returns the number of results that the query is able to
+ * return. The number of results is independent of the offset and limit
+ * options used when executing the query.
+ *
+ * @return The total number of results, or -1 if this information is not
+ * available.
+ */
+ long getTotal();
+
+ /**
+ * The name of the columns contained in the search result.
+ *
+ * @return An instance of {@code Iterable}, where each element represents
+ * the name of a column in the search result.
+ */
+ Iterable getColumns();
+
+ /**
+ * The name of the selectors involved in the query.
+ *
+ * @return An instance of {@code Iterable}, where each element represents
+ * the name of a selector involved in the query.
+ */
+ Iterable getSelectors();
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteRevision.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteRevision.java
new file mode 100644
index 0000000..bd1e954
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteRevision.java
@@ -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;
+
+/**
+ * A repository revision represents an immutable state of the repository.
+ *
+ * Having a revision allows you to perform repeatable reads, because it is
+ * assured that the state referenced by the revision is immutable.
+ *
+ * The revision also allows the system to reference a known state of the
+ * repository when changes are committed. Since a revision references an
+ * immutable state, committing some changes will create a new state of the
+ * repository that will be referenced by a new revision.
+ */
+public interface RemoteRevision {
+
+ /**
+ * Returns a string representation of the revision. This representation can
+ * be used as a reference by an external system and converted to a {@code
+ * RemoteRevision} object using {@link RemoteSession#readRevision(String)}.
+ *
+ * @return A string representation of the revision.
+ */
+ String asString();
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteSession.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteSession.java
new file mode 100644
index 0000000..521eb0d
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteSession.java
@@ -0,0 +1,227 @@
+/*
+ * 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;
+
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Collection of operations available on a remote repository once the user
+ * correctly logged in.
+ *
+ * Operations working on pure content, like reading a tree or committing
+ * changes, requires a revision. A revision represents a snapshot of the
+ * repository in a specified point in time. This concept enables repeatable
+ * reads and consistent writes.
+ *
+ * When binary data is involved, this interface exposes methods to read and
+ * write arbitrary binary data from and to the repository. A binary data is
+ * considered an immutable collection of bytes that can be referenced by an
+ * identifier.
+ */
+public interface RemoteSession {
+
+ /**
+ * Read the latest revision in the repository.
+ *
+ * This operation is always meant to succeed, because the repository will
+ * always have an initial revision to return to the caller.
+ *
+ * @return The latest revision in the repository.
+ */
+ RemoteRevision readLastRevision();
+
+ /**
+ * Read a revision given a string representation of the revision itself.
+ *
+ * This operation may fail for a number of reasons. In example, the string
+ * passed to this method is not a valid revision, or this string represents
+ * a revision that was valid in the past but it is no more valid.
+ *
+ * @param revision The string representation of the revision.
+ * @return The revision represented by the string passed to this method, or
+ * {@code null} if the string representation is invalid.
+ */
+ RemoteRevision readRevision(String revision);
+
+ /**
+ * Read a sub-tree from the repository at the given revision. Some filters
+ * may be applied to the tree to avoid reading unwanted information.
+ *
+ * @param revision The revision representing the state of the repository to
+ * read from.
+ * @param path The path of the root of the subtree to read.
+ * @param filters Filters to apply to the returned tree.
+ * @return The tree requested by the given path, filtered according to the
+ * provided filters. The method can return {@code null} if the root of the
+ * tree is not found in the repository for the given revision.
+ */
+ RemoteTree readTree(RemoteRevision revision, String path, RemoteTreeFilters filters);
+
+ /**
+ * Create an operation to represent the addition of a new node in the
+ * repository.
+ *
+ * @param path Path of the new node to create.
+ * @param properties Initial set of properties attached to the new node.
+ * @return An operation representing the addition of a new node.
+ */
+ RemoteOperation createAddOperation(String path, Map properties);
+
+ /**
+ * Create an operation representing the removal of an existing node from the
+ * repository.
+ *
+ * @param path Path of the node to remove.
+ * @return An operation representing the removal of an existing node.
+ */
+ RemoteOperation createRemoveOperation(String path);
+
+ /**
+ * Create an operation representing the creation or modification of a
+ * property of an existing node.
+ *
+ * @param path Path of the node where the property is or will be attached
+ * to.
+ * @param name Name of the property to set.
+ * @param value Value of the property.
+ * @return An operation representing the creation or modification of a
+ * property of an existing node.
+ */
+ RemoteOperation createSetOperation(String path, String name, RemoteValue value);
+
+ /**
+ * Create an operation to represent the removal of an existing property from
+ * an existing node in the repository.
+ *
+ * @param path Path of the node where the property is attached to.
+ * @param name Name of the property to remove.
+ * @return An operation representing the removal of a property.
+ */
+ RemoteOperation createUnsetOperation(String path, String name);
+
+ /**
+ * Create an operation to represent the copy of a subtree into another
+ * location into the repository.
+ *
+ * @param source Path of the root of the subtree to copy.
+ * @param target Path where the subtree should be copied to.
+ * @return An operation representing a copy of a subtree.
+ */
+ RemoteOperation createCopyOperation(String source, String target);
+
+ /**
+ * Create an operation to represent the move of a subtree into another
+ * location into the repository.
+ *
+ * @param source Path of the root of the source subtree to move.
+ * @param target Path where the subtree should be moved to.
+ * @return An operation representing a move of a subtree.
+ */
+ RemoteOperation createMoveOperation(String source, String target);
+
+ /**
+ * Create an operation that represents the aggregation of multiple, simpler
+ * operations. The aggregated operations are applied in the same sequence
+ * provided by this method.
+ *
+ * @param operations Sequence of operations to aggregate.
+ * @return An operation that, when executed, will execute the provided
+ * operations in the provided sequence.
+ */
+ RemoteOperation createAggregateOperation(List operations);
+
+ /**
+ * Commit some changes to the repository. The changes are represented by an
+ * operation. The operation will be applied on the repository state
+ * represented by the given revision.
+ *
+ * @param revision Revision where the changes should be applied to.
+ * @param operation Operation to change the state of the repository.
+ * @return A new revision representing the new state of the repository where
+ * the changes are applied.
+ * @throws RemoteCommitException if the provided operation can't be
+ * performed on the provided repository
+ * state.
+ */
+ RemoteRevision commit(RemoteRevision revision, RemoteOperation operation) throws RemoteCommitException;
+
+ /**
+ * Read a binary ID given a string representation of the binary ID itself.
+ *
+ * This operations may fail for a number of reasons. In example, the string
+ * doesn't represent a valid binary ID, or the string represents a binary ID
+ * that was valid in the past but is no more valid.
+ *
+ * @param binaryId String representation of the binary ID.
+ * @return The binary ID read from the repository. This method may return
+ * {@code null} if the string representation of the binary ID is not valid.
+ */
+ RemoteBinaryId readBinaryId(String binaryId);
+
+ /**
+ * Read a binary object from the repository according to the given filters.
+ *
+ * In the case of a binary object, filters are really simple. At most, it is
+ * possible to read just a portion of the binary object instead of reading
+ * it in its entirety.
+ *
+ * @param binaryId Binary ID referring to the binary object to read.
+ * @param filters Filters to apply to the returned binary object.
+ * @return A stream representing the filtered content of the binary object.
+ */
+ InputStream readBinary(RemoteBinaryId binaryId, RemoteBinaryFilters filters);
+
+ /**
+ * Read the length of a binary object from the repository.
+ *
+ * @param binaryId Binary ID referring to the binary object whose length
+ * should be read.
+ * @return The length of the binary object.
+ */
+ long readBinaryLength(RemoteBinaryId binaryId);
+
+ /**
+ * Write a binary object into the repository and return a binary ID
+ * referencing to it.
+ *
+ * @param stream Stream representing the binary object to write.
+ * @return Binary ID referencing the binary object written in the
+ * repository.
+ */
+ RemoteBinaryId writeBinary(InputStream stream);
+
+ /**
+ * Performs a search in the content and returns a set of search results.
+ *
+ * @param revision The revision that should be used when searching the
+ * content.
+ * @param query The query. It may contain placeholders that are to be
+ * substituted with the actual parameters.
+ * @param language The language the query is written in. It identifies the
+ * syntax of the query.
+ * @param offset How many rows to skip when returning the results.
+ * @param limit How many results to return.
+ * @return Search results.
+ * @throws RemoteQueryParseException if the query can't be correctly
+ * parsed.
+ */
+ RemoteResults search(RemoteRevision revision, String query, String language, long offset, long limit) throws RemoteQueryParseException;
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteTree.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteTree.java
new file mode 100644
index 0000000..0b8c37b
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteTree.java
@@ -0,0 +1,74 @@
+/*
+ * 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;
+
+import java.util.Map;
+
+/**
+ * This interface is a recursive data structure representing a view on a
+ * repository tree.
+ *
+ * The purpose of the remote tree is not to represent exactly the content as
+ * stored in the repository, but to provide a view on that content fulfilling
+ * the filtering options provided by the client when the tree was accessed.
+ */
+public interface RemoteTree {
+
+ /**
+ * Read the properties associated to the root of this remote tree. The root
+ * of this remote tree is represented by the instance of {@code RemoteTree}
+ * this method is invoked on.
+ *
+ * @return the properties associated to the root of this remote tree.
+ */
+ Map getProperties();
+
+ /**
+ * Read the children associated to the root of this remote tree. The root of
+ * this remote tree is represented by the instance of {@code RemoteTree}
+ * this method is invoked on. The children of this remote tree are
+ * themselves remote trees.
+ *
+ * The remote tree may be truncated at some point (e.g. to avoid very deep
+ * remote trees to be returned), and this is the reason why the values of
+ * this {@code Map} can be {@code null}. When a {@code null} value is met,
+ * the consumer of this interface must assume that there is another subtree
+ * rooted under the corresponding key, but it is not returned to fulfill the
+ * filtering options provided when this tree was read.
+ *
+ * @return The children associated to the root of this remote tree.
+ */
+ Map getChildren();
+
+ /**
+ * Return a flag to indicate that this remote tree actually has more
+ * children than the one returned by {@link #getChildren()}.
+ *
+ * This flag is important when the repository tree is read using very strict
+ * filtering options regarding the maximum number of children to return. If
+ * this method returns {@code true}, a consumer of this interface must
+ * assume that there are more children than the one attached to the root of
+ * this tree. They could be retrieved by varying the relevant filtering
+ * options and performing another read for this subtree.
+ *
+ * @return {@true} if this remote tree is not exposing the full set of
+ * children as stored in the repository, {@code false} otherwise.
+ */
+ boolean hasMoreChildren();
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteTreeFilters.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteTreeFilters.java
new file mode 100644
index 0000000..860a7d3
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteTreeFilters.java
@@ -0,0 +1,85 @@
+/*
+ * 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;
+
+import java.util.Set;
+
+import static com.google.common.collect.Sets.newHashSet;
+
+/**
+ * Represents a set of filters that can be applied when a subtree is read from
+ * the repository.
+ */
+public class RemoteTreeFilters {
+
+ /**
+ * Return the depth of the tree to read. This method returns {@code 0} by
+ * default, meaning that only the root of the subtree will be returned. The
+ * default value makes read operation for a subtree look like the read
+ * operation for a single node.
+ */
+ public int getDepth() {
+ return 0;
+ }
+
+ /**
+ * Return the property filters. This method returns {@code {"*"}} by
+ * default, meaning that every property will be returned for every node
+ * included in the subtree.
+ */
+ public Set getPropertyFilters() {
+ return newHashSet("*");
+ }
+
+ /**
+ * Return the node filters. This method returns a value of {@code {"*"}} by
+ * default, meaning that every descendant node of the root of the tree wil
+ * be returned.
+ */
+ public Set getNodeFilters() {
+ return newHashSet("*");
+ }
+
+ /**
+ * Return the binary threshold. This method returns {@code 0} by default,
+ * meaning that by default binary properties will be returned as references
+ * to binary objects, instead of being returned as proper binary objects.
+ */
+ public long getBinaryThreshold() {
+ return 0;
+ }
+
+ /**
+ * Return the start index for children. This method returns {@code 0} by
+ * default, meaning that children will be read from the beginning when
+ * reading the root node and every descendant.
+ */
+ public int getChildrenStart() {
+ return 0;
+ }
+
+ /**
+ * Return the maximum number of children to return. This method returns
+ * {@code -1} by default, meaning that every children will be returned when
+ * reading the root node and every descendant.
+ */
+ public int getChildrenCount() {
+ return -1;
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteValue.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteValue.java
new file mode 100644
index 0000000..321408d
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteValue.java
@@ -0,0 +1,1431 @@
+/*
+ * 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;
+
+import java.io.InputStream;
+import java.math.BigDecimal;
+
+/**
+ * Represents a value that can be assigned to a property in the repository.
+ * Client of the remote repository provides values as instances of this class.
+ */
+public class RemoteValue {
+
+ /**
+ * A generic interface to represent a supplier of an item.
+ *
+ * In the specific, it is used by values whose underlying implementation is
+ * an {@code InputStream}. To enable multiple traversals of {@code
+ * InputStream}s, the value is wrapped by this interface to effectively have
+ * a factory over the underlying {@code InputStream}.
+ *
+ * @param Type of the item this object is able to create.
+ */
+ public static interface Supplier {
+
+ T get();
+
+ }
+
+ /**
+ * This class helps executing logic that depends on the type of a remote
+ * value. Instead of manually branching code depending on the result of
+ * {@code isText}, {@code isBoolean} and so on, a handler can be implemented
+ * to provide different logic for different types.
+ */
+ public static class TypeHandler {
+
+ public void isBinary(Supplier value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isMultiBinary(Iterable> value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isBinaryId(String value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isMultiBinaryId(Iterable value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isBoolean(Boolean value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isMultiBoolean(Iterable value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isDate(Long value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isMultiDate(Iterable value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isDecimal(BigDecimal value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isMultiDecimal(Iterable value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isDouble(Double value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isMultiDouble(Iterable value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isLong(Long value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isMultiLong(Iterable value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isName(String value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isMultiName(Iterable value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isPath(String value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isMultiPath(Iterable value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isReference(String value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isMultiReference(Iterable value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isText(String value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isMultiText(Iterable value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isUri(String value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isMultiUri(Iterable value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isWeakReference(String value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isMultiWeakReference(Iterable value) {
+ throw new IllegalStateException("case not handled");
+ }
+
+ public void isUnknown() {
+ throw new IllegalStateException("case not handled");
+ }
+ }
+
+ /**
+ * Create a remote value of type string.
+ *
+ * @param value The string wrapped by the remote value.
+ * @return A remote value of type string wrapping the provided value.
+ */
+ public static RemoteValue toText(final String value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isText() {
+ return true;
+ }
+
+ @Override
+ public String asText() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote multi-value of type string.
+ *
+ * @param value The collection of strings wrapped by the remote value.
+ * @return A remote multi-value of type string wrapping the provided
+ * collection of strings.
+ */
+ public static RemoteValue toMultiText(final Iterable value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isMultiText() {
+ return true;
+ }
+
+ @Override
+ public Iterable asMultiText() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote value of type binary.
+ *
+ * @param value The factory of input streams wrapped by the remote value.
+ * @return A remote value of type binary wrapping the provided factory of
+ * input streams.
+ */
+ public static RemoteValue toBinary(final Supplier value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isBinary() {
+ return true;
+ }
+
+ @Override
+ public Supplier asBinary() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote multi-value of type binary.
+ *
+ * @param value The collection of factories of input streams wrapped by the
+ * remote value.
+ * @return A remote multi-value of type binary wrapping the provided
+ * collection of input streams.
+ */
+ public static RemoteValue toMultiBinary(final Iterable> value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isMultiBinary() {
+ return true;
+ }
+
+ @Override
+ public Iterable> asMultiBinary() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote value of type binary ID.
+ *
+ * @param value The binary ID wrapped by the remote value.
+ * @return A remote value wrapping the provided binary ID.
+ */
+ public static RemoteValue toBinaryId(final String value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isBinaryId() {
+ return true;
+ }
+
+ @Override
+ public String asBinaryId() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote multi-value of type binary ID.
+ *
+ * @param value The collection of binary IDs wrapped by the remote value.
+ * @return A remote multi-value wrapping the provided collection of binary
+ * IDs.
+ */
+ public static RemoteValue toMultiBinaryId(final Iterable value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isMultiBinaryId() {
+ return true;
+ }
+
+ @Override
+ public Iterable asMultiBinaryId() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote value of type long.
+ *
+ * @param value The long to wrap in a remote value.
+ * @return A remote value of type long wrapping the provided long value.
+ */
+ public static RemoteValue toLong(final long value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isLong() {
+ return true;
+ }
+
+
+ @Override
+ public Long asLong() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote multi-value of type long.
+ *
+ * @param value The collection of long values to wrap in a remote value.
+ * @return A remote multi-value of type long wrapping the provided
+ * collection of long values.
+ */
+ public static RemoteValue toMultiLong(final Iterable value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isMultiLong() {
+ return true;
+ }
+
+ @Override
+ public Iterable asMultiLong() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote value of type double.
+ *
+ * @param value The double value to wrap into a remote value.
+ * @return A remote value wrapping the provided remote value.
+ */
+ public static RemoteValue toDouble(final double value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isDouble() {
+ return true;
+ }
+
+ @Override
+ public Double asDouble() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote multi-value of type double.
+ *
+ * @param value The collection of double values to wrap into a remote
+ * value.
+ * @return A remote multi-value of type double wrapping the provided
+ * collection of double values.
+ */
+ public static RemoteValue toMultiDouble(final Iterable value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isMultiDouble() {
+ return true;
+ }
+
+ @Override
+ public Iterable asMultiDouble() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote value of type date.
+ *
+ * @param value The date to wrap into a remote value. The date is expressed
+ * in milliseconds since January 1, 1970, 00:00:00 GMT.
+ * @return A remote value of type date wrapping the provided date.
+ */
+ public static RemoteValue toDate(final long value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isDate() {
+ return true;
+ }
+
+ @Override
+ public Long asDate() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote multi-value of type date.
+ *
+ * @param value The collection of dates to wrap into a remote value. Every
+ * date is expressed in milliseconds since January 1, 1970,
+ * 00:00:00 GMT.
+ * @return A remote multi-value of type date wrapping the provided
+ * collection of dates.
+ */
+ public static RemoteValue toMultiDate(final Iterable value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isMultiDate() {
+ return true;
+ }
+
+ @Override
+ public Iterable asMultiDate() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote value of type boolean.
+ *
+ * @param value The boolean value to wrap into a remote value.
+ * @return A remote value wrapping the provided boolean value.
+ */
+ public static RemoteValue toBoolean(final boolean value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isBoolean() {
+ return true;
+ }
+
+ @Override
+ public Boolean asBoolean() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote multi-value of type boolean.
+ *
+ * @param value The collection of boolean values to wrap into a remote
+ * value.
+ * @return A remote value wrapping the provided collection of boolean
+ * values.
+ */
+ public static RemoteValue toMultiBoolean(final Iterable value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isMultiBoolean() {
+ return true;
+ }
+
+ @Override
+ public Iterable asMultiBoolean() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote value of type name.
+ *
+ * @param value The name to wrap into a remote value. A name is represented
+ * as a string.
+ * @return A remote value of type name wrapping the provided string.
+ */
+ public static RemoteValue toName(final String value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isName() {
+ return true;
+ }
+
+ @Override
+ public String asName() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote multi-value of type name.
+ *
+ * @param value The collection of names to wrap into a remote value. Every
+ * name is represented by a string.
+ * @return A remote multi-value of type name wrapping the provided
+ * collection of strings.
+ */
+ public static RemoteValue toMultiName(final Iterable value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isMultiName() {
+ return true;
+ }
+
+ @Override
+ public Iterable asMultiName() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote value of type path.
+ *
+ * @param value The path to wrap into the remote value. A path is
+ * represented by a string.
+ * @return A remote value of type path wrapping the provided string.
+ */
+ public static RemoteValue toPath(final String value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isPath() {
+ return true;
+ }
+
+ @Override
+ public String asPath() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote multi-value of type path.
+ *
+ * @param value The collection of paths to wrap into a remote value. Every
+ * path is represented by a string.
+ * @return A remote multi-value of type path wrapping the provided strings.
+ */
+ public static RemoteValue toMultiPath(final Iterable value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isMultiPath() {
+ return true;
+ }
+
+ @Override
+ public Iterable asMultiPath() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote value of type reference.
+ *
+ * @param value The reference to wrap in a remote value. The reference is
+ * represented by a string.
+ * @return A remote value of type reference wrapping the provided string.
+ */
+ public static RemoteValue toReference(final String value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isReference() {
+ return true;
+ }
+
+ @Override
+ public String asReference() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote multi-value of type reference.
+ *
+ * @param value The collection of references to wrap in a remote value.
+ * Every reference is represented by a string.
+ * @return A remote multi-value of type reference wrapping the provided
+ * collection of strings.
+ */
+ public static RemoteValue toMultiReference(final Iterable value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isMultiReference() {
+ return true;
+ }
+
+ @Override
+ public Iterable asMultiReference() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote value of type weak reference.
+ *
+ * @param value The weak reference to wrap into a remote value. The weak
+ * reference is represented by a string value.
+ * @return A remote value of type weak reference wrapping the provided
+ * string value.
+ */
+ public static RemoteValue toWeakReference(final String value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isWeakReference() {
+ return true;
+ }
+
+ @Override
+ public String asWeakReference() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote multi-value of type weak reference.
+ *
+ * @param value The collection of weak references to wrap into a remote
+ * value. Every weak reference is represented by a string.
+ * @return A remote multi-value of type weak reference wrapping the provided
+ * collection of strings.
+ */
+ public static RemoteValue toMultiWeakReference(final Iterable value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isMultiWeakReference() {
+ return true;
+ }
+
+ @Override
+ public Iterable asMultiWeakReference() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote value of type URI.
+ *
+ * @param value The string representation of the URI to wrap into a remote
+ * value.
+ * @return A remote value of type URI wrapping the provided string.
+ */
+ public static RemoteValue toUri(final String value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isUri() {
+ return true;
+ }
+
+ @Override
+ public String asUri() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote multi-value of type URI.
+ *
+ * @param value The collection of URIs to wrap into the remote value. Every
+ * URI is represented by a string.
+ * @return A remote multi-value of type URI wrapping the provided collection
+ * of strings.
+ */
+ public static RemoteValue toMultiUri(final Iterable value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isMultiUri() {
+ return true;
+ }
+
+ @Override
+ public Iterable asMultiUri() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote value of type decimal.
+ *
+ * @param value The decimal to wrap into a remote value.
+ * @return A remote value of type decimal wrapping the provided decimal
+ * value.
+ */
+ public static RemoteValue toDecimal(final BigDecimal value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isDecimal() {
+ return true;
+ }
+
+ @Override
+ public BigDecimal asDecimal() {
+ return value;
+ }
+
+ };
+ }
+
+ /**
+ * Create a remote multi-value of type decimal.
+ *
+ * @param value The collection of decimals to wrap into a remote value.
+ * @return A remote multi-value of type decimal wrapping the provided
+ * collection of decimals.
+ */
+ public static RemoteValue toMultiDecimal(final Iterable value) {
+ return new RemoteValue() {
+
+ @Override
+ public boolean isMultiDecimal() {
+ return true;
+ }
+
+ @Override
+ public Iterable asMultiDecimal() {
+ return value;
+ }
+
+ };
+ }
+
+ private RemoteValue() {
+
+ }
+
+ /**
+ * Check if this remote value is of type string.
+ *
+ * @return {@code true} if this remote value is of type string, {@code
+ * false} otherwise.
+ */
+ public boolean isText() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote string value.
+ *
+ * @return The string wrapped by this remote value if this remote value is
+ * of type string, {@code null} otherwise.
+ */
+ public String asText() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is a multi-value of type string.
+ *
+ * @return {@code true} if this remote value is a multi-value of type
+ * string, {@code false} otherwise.
+ */
+ public boolean isMultiText() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote string multi-value.
+ *
+ * @return The collection of strings wrapped by this remote value if this
+ * remote value is a multi-value of type string, {@code null} otherwise.
+ */
+ public Iterable asMultiText() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is of type binary.
+ *
+ * @return {@code true} if this remote value is of type binary, {@code
+ * false} otherwise.
+ */
+ public boolean isBinary() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote boolean value.
+ *
+ * @return The value of this remote value if this remote value is of type
+ * binary, {@code null} otherwise.
+ */
+ public Supplier asBinary() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is a multi-value of type binary.
+ *
+ * @return {@code true} if this remote value is a multi-value of type
+ * binary, {@code false} otherwise.
+ */
+ public boolean isMultiBinary() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote binary multi-value.
+ *
+ * @return The value of this remote value if this remote value is a
+ * multi-value of type binary, {@code null} otherwise.
+ */
+ public Iterable> asMultiBinary() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is of type binary ID.
+ *
+ * @return {@code true} if this remote value is of type binary ID, {@code
+ * false} otherwise.
+ */
+ public boolean isBinaryId() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote binary ID multi-value.
+ *
+ * @return The value of this remote value if this remote value is of type
+ * binary ID, {@code null} otherwise.
+ */
+ public String asBinaryId() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is a multi-value of type binary ID.
+ *
+ * @return {@code true} if this remote value is a multi-value of type binary
+ * ID, {@code false} otherwise.
+ */
+ public boolean isMultiBinaryId() {
+ return false;
+ }
+
+ /**
+ * Return the value of this remote binary ID multi-value.
+ *
+ * @return The value of this remote value if this remote value is a
+ * multi-value of type binary ID, {@code null} otherwise.
+ */
+ public Iterable asMultiBinaryId() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is of type long.
+ *
+ * @return {@code true} if this remote value is of type long, {@code false}
+ * otherwise.
+ */
+ public boolean isLong() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote long multi-value.
+ *
+ * @return The value of this remote value if this remote value is of type
+ * long, {@code null} otherwise.
+ */
+ public Long asLong() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is a multi-value of type long.
+ *
+ * @return {@code true} if this value is a multi-value of type long, {@code
+ * false} otherwise.
+ */
+ public boolean isMultiLong() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote multi-value of type long.
+ *
+ * @return The value of this remote value if this remote value is a
+ * multi-value of type long, {@code null} otherwise.
+ */
+ public Iterable asMultiLong() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is of type double.
+ *
+ * @return {@code true} if this remote value is of type long, {@code false}
+ * otherwise.
+ */
+ public boolean isDouble() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote value of type double.
+ *
+ * @return The value of this remote value if this remote value is of type
+ * double, {@code null} otherwise.
+ */
+ public Double asDouble() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is a multi-value of type double.
+ *
+ * @return {@code true} if this remote value is a multi-value of type
+ * double, {@code false} otherwise.
+ */
+ public boolean isMultiDouble() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote multi-value of type double.
+ *
+ * @return The value of this remote value if this remote value is a
+ * multi-value of type double, {@code null} otherwise.
+ */
+ public Iterable asMultiDouble() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is of type date.
+ *
+ * @return {@code true} if this remote value is of type date, {@code false}
+ * otherwise.
+ */
+ public boolean isDate() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote multi-value of type date.
+ *
+ * @return The value of this remote value if this remote value is of type
+ * date, {@code null} otherwise.
+ */
+ public Long asDate() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is a multi-value of type date.
+ *
+ * @return {@code true} if this remote value is a multi-value of type date,
+ * {@code false} otherwise.
+ */
+ public boolean isMultiDate() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote multi-value of type date.
+ *
+ * @return The value of this remote value if this remote value is a
+ * multi-value of type date, {@code null} otherwise.
+ */
+ public Iterable asMultiDate() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is of type boolean.
+ *
+ * @return {@code true} if this remote value is fo type boolean, {@code
+ * false} otherwise.
+ */
+ public boolean isBoolean() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote value of type boolean.
+ *
+ * @return The value of this remote value if this remote value is of type
+ * boolean, false otherwise.
+ */
+ public Boolean asBoolean() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is a multi-value of type boolean.
+ *
+ * @return {@code true} if this remote value is a multi-value of type
+ * boolean, {@code false} otherwise.
+ */
+ public boolean isMultiBoolean() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote multi-value of type boolean.
+ *
+ * @return The value of this remote value if this remote value is a
+ * multi-value of type boolean, {@code null} otherwise.
+ */
+ public Iterable asMultiBoolean() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is of type name.
+ *
+ * @return {@code true} if this remote value is of type name, {@code false}
+ * otherwise.
+ */
+ public boolean isName() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote value of type name.
+ *
+ * @return The value of this remote value if this remote value is of type
+ * name, {@code null} otherwise.
+ */
+ public String asName() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is a multi-value of type name.
+ *
+ * @return {@code true} if this remote value is a multi-value of type name,
+ * {@code false} otherwise.
+ */
+ public boolean isMultiName() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote multi-value of type name.
+ *
+ * @return The value of this remote value if this remote value is a
+ * multi-value of type name, {@code null} otherwise.
+ */
+ public Iterable asMultiName() {
+ return null;
+ }
+
+ /**
+ * Check if this value is of type path.
+ *
+ * @return {@code true} if this remote value is of type path, {@code false}
+ * otherwise.
+ */
+ public boolean isPath() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote value of type path.
+ *
+ * @return The value of this remote value if this remote value is of type
+ * path, {@code null} otherwise.
+ */
+ public String asPath() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is a multi-value of type path.
+ *
+ * @return {@code true} if this remote value is a multi-value of type path,
+ * {@code false} otherwise.
+ */
+ public boolean isMultiPath() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote multi-value of type path.
+ *
+ * @return The value of this remote value if this remote value is a
+ * multi-value of type path, {@code null} otherwise.
+ */
+ public Iterable asMultiPath() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is of type reference.
+ *
+ * @return {@code true} if this remote value is of type reference, {@code
+ * false} otherwise.
+ */
+ public boolean isReference() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote value of type reference.
+ *
+ * @return The value of this remote value if this remote value is of type
+ * reference, {@code null} otherwise.
+ */
+ public String asReference() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is a multi-value of type reference.
+ *
+ * @return {@code true} if this remote value is a multi-value of type
+ * reference, {@code false} otherwise.
+ */
+ public boolean isMultiReference() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote multi-value of type reference.
+ *
+ * @return The value of this remote value if this remote value is a
+ * multi-value of type reference, {@code null} otherwise.
+ */
+ public Iterable asMultiReference() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is of type weak reference.
+ *
+ * @return {@code true} if this remote value is fo type weak reference,
+ * {@code false} otherwise.
+ */
+ public boolean isWeakReference() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote value of type weak reference.
+ *
+ * @return The value of this remote value if this remote value is of type
+ * weak reference, {@code null} otherwise.
+ */
+ public String asWeakReference() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is a multi-value of type weak reference.
+ *
+ * @return {@code true} if this remote value is a multi-value of type weak
+ * reference, {@code false} otherwise.
+ */
+ public boolean isMultiWeakReference() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote multi-value of type weak reference.
+ *
+ * @return The value of this remote value if this remote value is a
+ * multi-value of type weak reference, {@code null} otherwise.
+ */
+ public Iterable asMultiWeakReference() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is of type decimal.
+ *
+ * @return {@code true} if this remote value is of type decimal, {@code
+ * false} otherwise.
+ */
+ public boolean isDecimal() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote value of type decimal.
+ *
+ * @return The value of this remote value if this remote value is of type
+ * decimal, {@code null} otherwise.
+ */
+ public BigDecimal asDecimal() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is a multi-value of type decimal.
+ *
+ * @return {@code true} if this remote value is a multi-value of type
+ * decimal, {@code false} otherwise.
+ */
+ public boolean isMultiDecimal() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote multi-value of type decimal.
+ *
+ * @return The value of this remote value if this remote value is a
+ * multi-value of type decimal, {@code null} otherwise.
+ */
+ public Iterable asMultiDecimal() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is of type URI.
+ *
+ * @return {@code true} if this remote value is of type URI, {@code false}
+ * otherwise.
+ */
+ public boolean isUri() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote value of type URI.
+ *
+ * @return The value of this remote value if this remote value is of type
+ * URI, {@code null} otherwise.
+ */
+ public String asUri() {
+ return null;
+ }
+
+ /**
+ * Check if this remote value is a multi-value of type URI.
+ *
+ * @return {@code true} if this remote value is a multi-value of type URI,
+ * {@code false} otherwise.
+ */
+ public boolean isMultiUri() {
+ return false;
+ }
+
+ /**
+ * Read the value of this remote multi-value of type URI.
+ *
+ * @return The value of this remote value if this remote value is a
+ * multi-value of type URI, {@code null} otherwise.
+ */
+ public Iterable asMultiUri() {
+ return null;
+ }
+
+ /**
+ * Calls a method of the provided handler according to the type of this
+ * remote value.
+ *
+ * @param handler Handler containing logic to be executing according to the
+ * type of this remote value.
+ */
+ public void whenType(TypeHandler handler) {
+ if (isBinary()) {
+ handler.isBinary(asBinary());
+ return;
+ }
+
+ if (isMultiBinary()) {
+ handler.isMultiBinary(asMultiBinary());
+ return;
+ }
+
+ if (isBinaryId()) {
+ handler.isBinaryId(asBinaryId());
+ return;
+ }
+
+ if (isMultiBinaryId()) {
+ handler.isMultiBinaryId(asMultiBinaryId());
+ return;
+ }
+
+ if (isBoolean()) {
+ handler.isBoolean(asBoolean());
+ return;
+ }
+
+ if (isMultiBoolean()) {
+ handler.isMultiBoolean(asMultiBoolean());
+ return;
+ }
+
+ if (isDate()) {
+ handler.isDate(asDate());
+ return;
+ }
+
+ if (isMultiDate()) {
+ handler.isMultiDate(asMultiDate());
+ return;
+ }
+
+ if (isDecimal()) {
+ handler.isDecimal(asDecimal());
+ return;
+ }
+
+ if (isMultiDecimal()) {
+ handler.isMultiDecimal(asMultiDecimal());
+ return;
+ }
+
+ if (isDouble()) {
+ handler.isDouble(asDouble());
+ return;
+ }
+
+ if (isMultiDouble()) {
+ handler.isMultiDouble(asMultiDouble());
+ return;
+ }
+
+ if (isLong()) {
+ handler.isLong(asLong());
+ return;
+ }
+
+ if (isMultiLong()) {
+ handler.isMultiLong(asMultiLong());
+ return;
+ }
+
+ if (isName()) {
+ handler.isName(asName());
+ return;
+ }
+
+ if (isMultiName()) {
+ handler.isMultiName(asMultiName());
+ return;
+ }
+
+ if (isPath()) {
+ handler.isPath(asPath());
+ return;
+ }
+
+ if (isMultiPath()) {
+ handler.isMultiPath(asMultiPath());
+ return;
+ }
+
+ if (isReference()) {
+ handler.isReference(asReference());
+ return;
+ }
+
+ if (isMultiReference()) {
+ handler.isMultiReference(asMultiReference());
+ return;
+ }
+
+ if (isText()) {
+ handler.isText(asText());
+ return;
+ }
+
+ if (isMultiText()) {
+ handler.isMultiText(asMultiText());
+ return;
+ }
+
+ if (isUri()) {
+ handler.isUri(asUri());
+ return;
+ }
+
+ if (isMultiUri()) {
+ handler.isMultiUri(asMultiUri());
+ return;
+ }
+
+ if (isWeakReference()) {
+ handler.isWeakReference(asWeakReference());
+ return;
+ }
+
+ if (isMultiWeakReference()) {
+ handler.isMultiWeakReference(asMultiWeakReference());
+ return;
+ }
+
+ handler.isUnknown();
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/AddContentRemoteOperation.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/AddContentRemoteOperation.java
new file mode 100644
index 0000000..912ca5d
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/AddContentRemoteOperation.java
@@ -0,0 +1,55 @@
+/*
+ * 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 AddContentRemoteOperation implements ContentRemoteOperation {
+
+ private static final Logger logger = LoggerFactory.getLogger(AddContentRemoteOperation.class);
+
+ private final String path;
+
+ public AddContentRemoteOperation(String path) {
+ this.path = path;
+ }
+
+ @Override
+ public void apply(Root root) throws RemoteCommitException {
+ logger.debug("performing 'add' operation on path={}", path);
+
+ Tree tree = root.getTree(path);
+
+ if (tree.exists()) {
+ throw new RemoteCommitException("node already exists");
+ }
+
+ Tree parent = tree.getParent();
+
+ if (!parent.exists()) {
+ throw new RemoteCommitException("parent node does not exist");
+ }
+
+ parent.addChild(tree.getName());
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/AggregateContentRemoteOperation.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/AggregateContentRemoteOperation.java
new file mode 100644
index 0000000..f23ac61
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/AggregateContentRemoteOperation.java
@@ -0,0 +1,40 @@
+/*
+ * 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 java.util.List;
+
+class AggregateContentRemoteOperation implements ContentRemoteOperation {
+
+ private final List operations;
+
+ public AggregateContentRemoteOperation(List operations) {
+ this.operations = operations;
+ }
+
+ @Override
+ public void apply(Root root) throws RemoteCommitException {
+ for (ContentRemoteOperation operation : operations) {
+ operation.apply(root);
+ }
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/BasicContentRemoteCredentials.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/BasicContentRemoteCredentials.java
new file mode 100644
index 0000000..ab6da4d
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/BasicContentRemoteCredentials.java
@@ -0,0 +1,54 @@
+/*
+ * 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.ContentRepository;
+import org.apache.jackrabbit.oak.api.ContentSession;
+import org.apache.jackrabbit.oak.remote.RemoteLoginException;
+
+import javax.jcr.NoSuchWorkspaceException;
+import javax.jcr.SimpleCredentials;
+import javax.security.auth.login.LoginException;
+
+class BasicContentRemoteCredentials implements ContentRemoteCredentials {
+
+ private final String user;
+
+ private final char[] password;
+
+ public BasicContentRemoteCredentials(String user, char[] password) {
+ this.user = user;
+ this.password = password;
+ }
+
+ @Override
+ public ContentSession login(ContentRepository repository) throws RemoteLoginException {
+ ContentSession session;
+
+ try {
+ session = repository.login(new SimpleCredentials(user, password), null);
+ } catch (LoginException e) {
+ throw new RemoteLoginException("unable to login", e);
+ } catch (NoSuchWorkspaceException e) {
+ throw new RemoteLoginException("unable to use the default workspace", e);
+ }
+
+ return session;
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteBinaries.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteBinaries.java
new file mode 100644
index 0000000..a163895
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteBinaries.java
@@ -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.content;
+
+import com.google.common.collect.Maps;
+import org.apache.jackrabbit.oak.api.Blob;
+
+import java.util.Map;
+import java.util.UUID;
+
+class ContentRemoteBinaries {
+
+ private Map binaries;
+
+ public ContentRemoteBinaries() {
+ binaries = Maps.newHashMap();
+ }
+
+ public String put(Blob blob) {
+ String binaryId = UUID.randomUUID().toString();
+
+ binaries.put(binaryId, blob);
+
+ return binaryId;
+ }
+
+ public Blob get(String binaryId) {
+ return binaries.get(binaryId);
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteBinaryId.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteBinaryId.java
new file mode 100644
index 0000000..952c1a0
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteBinaryId.java
@@ -0,0 +1,43 @@
+/*
+ * 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.remote.RemoteBinaryId;
+
+class ContentRemoteBinaryId implements RemoteBinaryId {
+
+ private final String reference;
+
+ private final Blob blob;
+
+ public ContentRemoteBinaryId(String reference, Blob blob) {
+ this.reference = reference;
+ this.blob = blob;
+ }
+
+ public Blob asBlob() {
+ return blob;
+ }
+
+ @Override
+ public String asString() {
+ return reference;
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteCredentials.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteCredentials.java
new file mode 100644
index 0000000..f2a9303
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteCredentials.java
@@ -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.content;
+
+import org.apache.jackrabbit.oak.api.ContentRepository;
+import org.apache.jackrabbit.oak.api.ContentSession;
+import org.apache.jackrabbit.oak.remote.RemoteCredentials;
+import org.apache.jackrabbit.oak.remote.RemoteLoginException;
+
+interface ContentRemoteCredentials extends RemoteCredentials {
+
+ ContentSession login(ContentRepository repository) throws RemoteLoginException;
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteInputStream.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteInputStream.java
new file mode 100644
index 0000000..bcf667b
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteInputStream.java
@@ -0,0 +1,72 @@
+/*
+ * 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.remote.RemoteBinaryFilters;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+class ContentRemoteInputStream extends InputStream {
+
+ private final InputStream stream;
+
+ private final long start;
+
+ private final long count;
+
+ private long index = 0;
+
+ public ContentRemoteInputStream(InputStream stream, RemoteBinaryFilters filters) {
+ this.stream = stream;
+
+ long startFilter = filters.getStart();
+
+ if (startFilter > 0) {
+ this.start = startFilter;
+ } else {
+ this.start = 0;
+ }
+
+ this.count = filters.getCount();
+ }
+
+ @Override
+ public int read() throws IOException {
+ while (index < start - 1) {
+ long skipped = stream.skip(start - index);
+
+ if (skipped <= 0) {
+ return -1;
+ }
+
+ index = index + skipped;
+ }
+
+ if (count >= 0 && index >= start + count) {
+ return -1;
+ }
+
+ int result = stream.read();
+
+ index = index + 1;
+
+ return result;
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteOperation.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteOperation.java
new file mode 100644
index 0000000..a608a11
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteOperation.java
@@ -0,0 +1,28 @@
+/*
+ * 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.apache.jackrabbit.oak.remote.RemoteOperation;
+
+interface ContentRemoteOperation extends RemoteOperation {
+
+ void apply(Root root) throws RemoteCommitException;
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRepository.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRepository.java
new file mode 100644
index 0000000..1b7abda
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRepository.java
@@ -0,0 +1,83 @@
+/*
+ * 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.Oak;
+import org.apache.jackrabbit.oak.api.ContentRepository;
+import org.apache.jackrabbit.oak.api.ContentSession;
+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 java.util.Set;
+
+public class ContentRemoteRepository implements RemoteRepository {
+
+ private final ContentRepository contentRepository;
+
+ private final ContentRemoteRevisions contentRemoteRevisions;
+
+ private final ContentRemoteBinaries contentRemoteBinaries;
+
+ public ContentRemoteRepository(ContentRepository contentRepository) {
+ this.contentRemoteRevisions = new ContentRemoteRevisions();
+ this.contentRemoteBinaries = new ContentRemoteBinaries();
+ this.contentRepository = contentRepository;
+ }
+
+ @Override
+ public RemoteCredentials createBasicCredentials(final String user, final char[] password) {
+ return new BasicContentRemoteCredentials(user, password);
+ }
+
+ @Override
+ public RemoteCredentials createImpersonationCredentials(Set principals) {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public RemoteSession login(RemoteCredentials remoteCredentials) throws RemoteLoginException {
+ ContentRemoteCredentials contentRemoteCredentials = null;
+
+ if (remoteCredentials instanceof ContentRemoteCredentials) {
+ contentRemoteCredentials = (ContentRemoteCredentials) remoteCredentials;
+ }
+
+ if (contentRemoteCredentials == null) {
+ throw new IllegalArgumentException("invalid credentials");
+ }
+
+ Thread thread = Thread.currentThread();
+
+ ClassLoader loader = thread.getContextClassLoader();
+
+ thread.setContextClassLoader(Oak.class.getClassLoader());
+
+ ContentSession session;
+
+ try {
+ session = contentRemoteCredentials.login(contentRepository);
+ } finally {
+ thread.setContextClassLoader(loader);
+ }
+
+ return new ContentRemoteSession(session, contentRemoteRevisions, contentRemoteBinaries);
+ }
+
+}
\ No newline at end of file
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResult.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResult.java
new file mode 100644
index 0000000..d518f2c
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResult.java
@@ -0,0 +1,154 @@
+/*
+ * 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.PropertyValue;
+import org.apache.jackrabbit.oak.api.ResultRow;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.remote.RemoteResult;
+import org.apache.jackrabbit.oak.remote.RemoteValue;
+import org.apache.jackrabbit.util.ISO8601;
+
+import javax.jcr.PropertyType;
+import java.util.List;
+
+import static com.google.common.collect.Lists.newArrayList;
+
+class ContentRemoteResult implements RemoteResult {
+
+ private final ContentRemoteBinaries binaries;
+
+ private final ResultRow row;
+
+ public ContentRemoteResult(ContentRemoteBinaries binaries, ResultRow row) {
+ this.binaries = binaries;
+ this.row = row;
+ }
+
+ @Override
+ public RemoteValue getColumnValue(String column) {
+ return toRemoteValue(row.getValue(column));
+ }
+
+ private RemoteValue toRemoteValue(PropertyValue value) {
+ if (value == null) {
+ return null;
+ }
+
+ Type> type = value.getType();
+
+ if (type.isArray()) {
+ return toMultiRemoteValue(value);
+ } else {
+ return toSingleRemoteValue(value);
+ }
+ }
+
+ private RemoteValue toSingleRemoteValue(PropertyValue value) {
+ Type> type = value.getType();
+
+ switch (type.tag()) {
+ case PropertyType.STRING:
+ return RemoteValue.toText(value.getValue(Type.STRING));
+ case PropertyType.BINARY:
+ return RemoteValue.toBinaryId(binaries.put(value.getValue(Type.BINARY)));
+ case PropertyType.LONG:
+ return RemoteValue.toLong(value.getValue(Type.LONG));
+ case PropertyType.DOUBLE:
+ return RemoteValue.toDouble(value.getValue(Type.DOUBLE));
+ case PropertyType.DATE:
+ return RemoteValue.toDate(ISO8601.parse(value.getValue(Type.DATE)).getTimeInMillis());
+ case PropertyType.BOOLEAN:
+ return RemoteValue.toBoolean(value.getValue(Type.BOOLEAN));
+ case PropertyType.NAME:
+ return RemoteValue.toName(value.getValue(Type.NAME));
+ case PropertyType.PATH:
+ return RemoteValue.toPath(value.getValue(Type.PATH));
+ case PropertyType.REFERENCE:
+ return RemoteValue.toReference(value.getValue(Type.REFERENCE));
+ case PropertyType.WEAKREFERENCE:
+ return RemoteValue.toWeakReference(value.getValue(Type.WEAKREFERENCE));
+ case PropertyType.URI:
+ return RemoteValue.toUri(value.getValue(Type.URI));
+ case PropertyType.DECIMAL:
+ return RemoteValue.toDecimal(value.getValue(Type.DECIMAL));
+ }
+
+ throw new IllegalStateException("type not supported");
+ }
+
+ private RemoteValue toMultiRemoteValue(PropertyValue value) {
+ Type> type = value.getType();
+
+ switch (type.tag()) {
+ case PropertyType.STRING:
+ return RemoteValue.toMultiText(value.getValue(Type.STRINGS));
+ case PropertyType.BINARY:
+ return RemoteValue.toMultiBinaryId(readBinaryValues(value));
+ case PropertyType.LONG:
+ return RemoteValue.toMultiLong(value.getValue(Type.LONGS));
+ case PropertyType.DOUBLE:
+ return RemoteValue.toMultiDouble(value.getValue(Type.DOUBLES));
+ case PropertyType.DATE:
+ return RemoteValue.toMultiDate(readDateValues(value));
+ case PropertyType.BOOLEAN:
+ return RemoteValue.toMultiBoolean(value.getValue(Type.BOOLEANS));
+ case PropertyType.NAME:
+ return RemoteValue.toMultiName(value.getValue(Type.NAMES));
+ case PropertyType.PATH:
+ return RemoteValue.toMultiPath(value.getValue(Type.PATHS));
+ case PropertyType.REFERENCE:
+ return RemoteValue.toMultiReference(value.getValue(Type.REFERENCES));
+ case PropertyType.WEAKREFERENCE:
+ return RemoteValue.toMultiWeakReference(value.getValue(Type.WEAKREFERENCES));
+ case PropertyType.URI:
+ return RemoteValue.toMultiUri(value.getValue(Type.URIS));
+ case PropertyType.DECIMAL:
+ return RemoteValue.toMultiDecimal(value.getValue(Type.DECIMALS));
+ }
+
+ throw new IllegalStateException("type not supported");
+ }
+
+ private Iterable readBinaryValues(PropertyValue value) {
+ List result = newArrayList();
+
+ for (Blob blob : value.getValue(Type.BINARIES)) {
+ result.add(binaries.put(blob));
+ }
+
+ return result;
+ }
+
+ private Iterable readDateValues(PropertyValue value) {
+ List result = newArrayList();
+
+ for (String string : value.getValue(Type.DATES)) {
+ result.add(ISO8601.parse(string).getTimeInMillis());
+ }
+
+ return result;
+ }
+
+ @Override
+ public String getSelectorPath(String selector) {
+ return row.getPath(selector);
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResults.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResults.java
new file mode 100644
index 0000000..ae1da11
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResults.java
@@ -0,0 +1,72 @@
+/*
+ * 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.Result;
+import org.apache.jackrabbit.oak.api.ResultRow;
+import org.apache.jackrabbit.oak.remote.RemoteResult;
+import org.apache.jackrabbit.oak.remote.RemoteResults;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import static com.google.common.collect.Lists.newArrayList;
+
+class ContentRemoteResults implements RemoteResults {
+
+ private final ContentRemoteBinaries binaries;
+
+ private final Result results;
+
+ public ContentRemoteResults(ContentRemoteBinaries binaries, Result results) {
+ this.binaries = binaries;
+ this.results = results;
+ }
+
+ @Override
+ public long getTotal() {
+ return results.getSize();
+ }
+
+ @Override
+ public Iterable getColumns() {
+ return Arrays.asList(results.getColumnNames());
+ }
+
+ @Override
+ public Iterable getSelectors() {
+ return Arrays.asList(results.getSelectorNames());
+ }
+
+ @Override
+ public Iterator iterator() {
+ return getResults().iterator();
+ }
+
+ private Iterable getResults() {
+ List results = newArrayList();
+
+ for (ResultRow row : this.results.getRows()) {
+ results.add(new ContentRemoteResult(binaries, row));
+ }
+
+ return results;
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRevision.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRevision.java
new file mode 100644
index 0000000..483e71e
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRevision.java
@@ -0,0 +1,43 @@
+/*
+ * 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.RemoteRevision;
+
+class ContentRemoteRevision implements RemoteRevision {
+
+ private final String id;
+
+ private final Root root;
+
+ public ContentRemoteRevision(String id, Root root) {
+ this.id = id;
+ this.root = root;
+ }
+
+ @Override
+ public String asString() {
+ return id;
+ }
+
+ public Root getRoot() {
+ return root;
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRevisions.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRevisions.java
new file mode 100644
index 0000000..8d9ac0e
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRevisions.java
@@ -0,0 +1,106 @@
+/*
+ * 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.Objects;
+import com.google.common.collect.Maps;
+import org.apache.jackrabbit.oak.api.AuthInfo;
+import org.apache.jackrabbit.oak.api.Root;
+
+import java.security.Principal;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+class ContentRemoteRevisions {
+
+ private class Key {
+
+ private final String revisionId;
+
+ private final Set principals;
+
+ private final String user;
+
+ private Key(AuthInfo authInfo, String revisionId) {
+ this.user = authInfo.getUserID();
+ this.principals = authInfo.getPrincipals();
+ this.revisionId = revisionId;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object == null) {
+ return false;
+ }
+
+ if (object == this) {
+ return true;
+ }
+
+ if (getClass() != object.getClass()) {
+ return false;
+ }
+
+ Key other = (Key) object;
+
+ if (!Objects.equal(revisionId, other.revisionId)) {
+ return false;
+ }
+
+ if (!Objects.equal(user, other.user)) {
+ return false;
+ }
+
+ if (!Objects.equal(principals, other.principals)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(revisionId, user, principals);
+ }
+
+ }
+
+ private Map roots;
+
+ public ContentRemoteRevisions() {
+ this.roots = Maps.newHashMap();
+ }
+
+ private Key key(AuthInfo authInfo, String revisionId) {
+ return new Key(authInfo, revisionId);
+ }
+
+ public Root get(AuthInfo authInfo, String revisionId) {
+ return roots.get(key(authInfo, revisionId));
+ }
+
+ public String put(AuthInfo authInfo, Root root) {
+ String revisionId = UUID.randomUUID().toString();
+
+ roots.put(key(authInfo, revisionId), root);
+
+ return revisionId;
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteSession.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteSession.java
new file mode 100644
index 0000000..15f9cc5
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteSession.java
@@ -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 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 operations = new ArrayList();
+
+ operations.add(new AddContentRemoteOperation(path));
+
+ for (Map.Entry 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 operations) {
+ if (operations == null) {
+ throw new IllegalArgumentException("operations not provided");
+ }
+
+ List contentRemoteOperations = new ArrayList();
+
+ 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(), new HashMap());
+ } catch (ParseException e) {
+ throw new RemoteQueryParseException("invalid query", e);
+ }
+
+ return new ContentRemoteResults(contentRemoteBinaries, results);
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteTree.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteTree.java
new file mode 100644
index 0000000..427a536
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteTree.java
@@ -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 getProperties() {
+ Map properties = new HashMap();
+
+ 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() {
+
+ @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 getDates(Iterable dates) {
+ return Iterables.transform(dates, new Function() {
+
+ @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 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 blobs) {
+ long length = 0;
+
+ for (Blob blob : blobs) {
+ length = length + blob.length();
+ }
+
+ return length;
+ }
+
+ private Supplier getBinary(final Blob blob) {
+ return new Supplier() {
+
+ @Override
+ public InputStream get() {
+ return blob.getNewStream();
+ }
+
+ };
+ }
+
+ private Iterable> getBinaries(Iterable blobs) {
+ return Iterables.transform(blobs, new Function>() {
+
+ @Override
+ public Supplier apply(Blob blob) {
+ return getBinary(blob);
+ }
+
+ });
+ }
+
+ private String getBinaryId(Blob blob) {
+ return contentRemoteBinaries.put(blob);
+ }
+
+ private Iterable getBinaryIds(Iterable blobs) {
+ return Iterables.transform(blobs, new Function() {
+
+ @Override
+ public String apply(Blob blob) {
+ return getBinaryId(blob);
+ }
+
+ });
+ }
+
+ @Override
+ public Map getChildren() {
+ Map children = new HashMap();
+
+ 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 getFilteredChildren() {
+ Iterable 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 getNodeFilters() {
+ return new Predicate() {
+
+ @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;
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/CopyContentRemoteOperation.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/CopyContentRemoteOperation.java
new file mode 100644
index 0000000..ed23b06
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/CopyContentRemoteOperation.java
@@ -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());
+ }
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/MoveContentRemoteOperation.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/MoveContentRemoteOperation.java
new file mode 100644
index 0000000..9199d5e
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/MoveContentRemoteOperation.java
@@ -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");
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/RemoveContentRemoteOperation.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/RemoveContentRemoteOperation.java
new file mode 100644
index 0000000..177a285
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/RemoveContentRemoteOperation.java
@@ -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");
+ }
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetContentRemoteOperation.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetContentRemoteOperation.java
new file mode 100644
index 0000000..c805304
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetContentRemoteOperation.java
@@ -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));
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetPropertyHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetPropertyHandler.java
new file mode 100644
index 0000000..5b4a323
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetPropertyHandler.java
@@ -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 value) {
+ tree.setProperty(name, getBlob(root, value), Type.BINARY);
+ }
+
+ @Override
+ public void isMultiBinary(Iterable> 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 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 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 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 value) {
+ tree.setProperty(name, value, Type.DECIMALS);
+ }
+
+ @Override
+ public void isDouble(Double value) {
+ tree.setProperty(name, value, Type.DOUBLE);
+ }
+
+ @Override
+ public void isMultiDouble(Iterable value) {
+ tree.setProperty(name, value, Type.DOUBLES);
+ }
+
+ @Override
+ public void isLong(Long value) {
+ tree.setProperty(name, value, Type.LONG);
+ }
+
+ @Override
+ public void isMultiLong(Iterable value) {
+ tree.setProperty(name, value, Type.LONGS);
+ }
+
+ @Override
+ public void isName(String value) {
+ tree.setProperty(name, value, Type.NAME);
+ }
+
+ @Override
+ public void isMultiName(Iterable value) {
+ tree.setProperty(name, value, Type.NAMES);
+ }
+
+ @Override
+ public void isPath(String value) {
+ tree.setProperty(name, value, Type.PATH);
+ }
+
+ @Override
+ public void isMultiPath(Iterable value) {
+ tree.setProperty(name, value, Type.PATHS);
+ }
+
+ @Override
+ public void isReference(String value) {
+ tree.setProperty(name, value, Type.REFERENCE);
+ }
+
+ @Override
+ public void isMultiReference(Iterable value) {
+ tree.setProperty(name, value, Type.REFERENCES);
+ }
+
+ @Override
+ public void isText(String value) {
+ tree.setProperty(name, value, Type.STRING);
+ }
+
+ @Override
+ public void isMultiText(Iterable value) {
+ tree.setProperty(name, value, Type.STRINGS);
+ }
+
+ @Override
+ public void isUri(String value) {
+ tree.setProperty(name, value, Type.URI);
+ }
+
+ @Override
+ public void isMultiUri(Iterable value) {
+ tree.setProperty(name, value, Type.URIS);
+ }
+
+ @Override
+ public void isWeakReference(String value) {
+ tree.setProperty(name, value, Type.WEAKREFERENCE);
+ }
+
+ @Override
+ public void isMultiWeakReference(Iterable value) {
+ tree.setProperty(name, value, Type.WEAKREFERENCES);
+ }
+
+ private Blob getBlob(Root root, Supplier 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 getBlobs(final Root root, Iterable> suppliers) {
+ return Iterables.transform(suppliers, new Function, Blob>() {
+
+ @Override
+ public Blob apply(Supplier supplier) {
+ return getBlob(root, supplier);
+ }
+
+ });
+ }
+
+ private Blob getBlobFromId(ContentRemoteBinaries binaries, String binaryId) {
+ return binaries.get(binaryId);
+ }
+
+ private Iterable getBlobsFromIds(final ContentRemoteBinaries binaries, Iterable binaryIds) {
+ return Iterables.transform(binaryIds, new Function() {
+
+ @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 getDates(Iterable times) {
+ return Iterables.transform(times, new Function() {
+
+ @Override
+ public String apply(Long time) {
+ return getDate(time);
+ }
+
+ });
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/UnsetContentRemoteOperation.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/UnsetContentRemoteOperation.java
new file mode 100644
index 0000000..c60aa92
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/UnsetContentRemoteOperation.java
@@ -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);
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filter.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filter.java
new file mode 100644
index 0000000..1b37681
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filter.java
@@ -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
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filters.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filters.java
new file mode 100644
index 0000000..baf77a5
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filters.java
@@ -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 includes = new HashSet();
+
+ private Set excludes = new HashSet();
+
+ public Filters(Set 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
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteHandler.java
new file mode 100644
index 0000000..d9a11fe
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteHandler.java
@@ -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);
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteServlet.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteServlet.java
new file mode 100644
index 0000000..1e0e77f
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteServlet.java
@@ -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 handlers, HttpServletRequest request, Handler otherwise) {
+ for (RemoteHandler handler : handlers) {
+ if (handler.match(request)) {
+ return handler;
+ }
+ }
+
+ return otherwise;
+ }
+
+ private Iterable 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 handlers(RemoteHandler... handlers) {
+ return Arrays.asList(handlers);
+ }
+
+ private RemoteHandler handler(String method, String path, Handler handler) {
+ return new RemoteHandler(matchesRequest(method, path), handler);
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/AuthenticationWrapperHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/AuthenticationWrapperHandler.java
new file mode 100644
index 0000000..0f14a96
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/AuthenticationWrapperHandler.java
@@ -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;
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetBinaryHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetBinaryHandler.java
new file mode 100644
index 0000000..0e48e8c
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetBinaryHandler.java
@@ -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 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
+ *
+ * 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
+ *
+ * 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
+ *
+ * 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.
+ *
+ * This handler is used when multiple ranges are requested.
+ */
+ private void handleMultipleRanges(HttpServletResponse response, RemoteSession session, RemoteBinaryId binaryId, List ranges) throws IOException {
+
+ String header;
+
+ long rangeStart, rangeEnd, fileLength, contentLength;
+
+ fileLength = session.readBinaryLength(binaryId);
+
+ // Compute response content length
+ // Create multipart headers
+
+ contentLength = 0;
+
+ List multipartHeaders = new ArrayList(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 rangeIt = ranges.iterator();
+ Iterator 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 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 ranges = new LinkedList();
+
+ 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.
+ *
+ * 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;
+ }
+ };
+ }
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastRevisionHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastRevisionHandler.java
new file mode 100644
index 0000000..9f828cf
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastRevisionHandler.java
@@ -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();
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastTreeHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastTreeHandler.java
new file mode 100644
index 0000000..dd6ef8d
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastTreeHandler.java
@@ -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();
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetRevisionTreeHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetRevisionTreeHandler.java
new file mode 100644
index 0000000..ee8ef8c
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetRevisionTreeHandler.java
@@ -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");
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetTreeHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetTreeHandler.java
new file mode 100644
index 0000000..3d91e15
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetTreeHandler.java
@@ -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 children) throws IOException {
+ generator.writeStartObject();
+
+ for (Map.Entry entry : children.entrySet()) {
+ generator.writeFieldName(entry.getKey());
+ renderResponse(generator, entry.getValue());
+ }
+
+ generator.writeEndObject();
+ }
+
+ private void renderProperties(JsonGenerator generator, Map properties) throws IOException {
+ generator.writeStartObject();
+
+ for (Map.Entry 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 getPropertyFilters() {
+ Set propertyFilters = readPropertyFilters(request);
+
+ if (propertyFilters == null) {
+ return super.getPropertyFilters();
+ }
+
+ return propertyFilters;
+ }
+
+ @Override
+ public Set getNodeFilters() {
+ Set 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 readPropertyFilters(HttpServletRequest request) {
+ return readSetParameter(request, "properties");
+ }
+
+ private Set 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 readSetParameter(HttpServletRequest request, String name) {
+ String[] values = request.getParameterValues(name);
+
+ if (values == null) {
+ return null;
+ }
+
+ return Sets.newHashSet(values);
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/Handler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/Handler.java
new file mode 100644
index 0000000..79bda5f
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/Handler.java
@@ -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;
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/Handlers.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/Handlers.java
new file mode 100644
index 0000000..6336cb2
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/Handlers.java
@@ -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();
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadBinaryHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadBinaryHandler.java
new file mode 100644
index 0000000..db13b47
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadBinaryHandler.java
@@ -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");
+ }
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadLastTreeHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadLastTreeHandler.java
new file mode 100644
index 0000000..9b1a45c
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadLastTreeHandler.java
@@ -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();
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadRevisionTreeHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadRevisionTreeHandler.java
new file mode 100644
index 0000000..aa9efc1
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadRevisionTreeHandler.java
@@ -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");
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadTreeHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadTreeHandler.java
new file mode 100644
index 0000000..b5a785c
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadTreeHandler.java
@@ -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");
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/NotFoundHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/NotFoundHandler.java
new file mode 100644
index 0000000..339c038
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/NotFoundHandler.java
@@ -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");
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchLastRevisionHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchLastRevisionHandler.java
new file mode 100644
index 0000000..6df4132
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchLastRevisionHandler.java
@@ -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();
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchRevisionHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchRevisionHandler.java
new file mode 100644
index 0000000..ff86b28
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchRevisionHandler.java
@@ -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 operations = new ArrayList();
+
+ 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 parsePropertiesField(JsonNode node, String name) {
+ return parseProperties(node.get(name));
+ }
+
+ private Map parseProperties(JsonNode node) {
+ Map values = new HashMap();
+
+ Iterator> iterator = node.fields();
+
+ while (iterator.hasNext()) {
+ Map.Entry 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 parseDecimalArrayField(JsonNode node, String field) {
+ return parseDecimalArray(node.get(field));
+ }
+
+ private Iterable parseDecimalArray(JsonNode node) {
+ List result = new ArrayList();
+
+ 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 parseBooleanArrayField(JsonNode node, String field) {
+ return parseBooleanArray(node.get(field));
+ }
+
+ private Iterable parseBooleanArray(JsonNode node) {
+ List result = new ArrayList();
+
+ 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 parseDoubleArrayField(JsonNode node, String field) {
+ return parseDoubleArray(node.get(field));
+ }
+
+ private Iterable parseDoubleArray(JsonNode node) {
+ List result = new ArrayList();
+
+ 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 parseLongArrayField(JsonNode node, String field) {
+ return parseLongArray(node.get(field));
+ }
+
+ private Iterable parseLongArray(JsonNode node) {
+ List result = new ArrayList();
+
+ for (JsonNode element : node) {
+ result.add(parseLong(element));
+ }
+
+ return result;
+ }
+
+ private Supplier parseBinaryField(JsonNode node, String field) {
+ return parseBinary(node.get(field));
+ }
+
+ private Supplier parseBinary(final JsonNode node) {
+ return new Supplier() {
+
+ @Override
+ public InputStream get() {
+ return new ByteArrayInputStream(Base64.decode(node.asText()).getBytes());
+ }
+
+ };
+ }
+
+ private Iterable> parseBinaryArrayField(JsonNode node, String field) {
+ return parseBinaryArray(node.get(field));
+ }
+
+ private Iterable> parseBinaryArray(JsonNode node) {
+ List> result = new ArrayList>();
+
+ for (JsonNode element : node) {
+ result.add(parseBinary(element));
+ }
+
+ return result;
+ }
+
+ private Iterable parseStringArrayField(JsonNode node, String field) {
+ return parseStringArray(node.get(field));
+ }
+
+ private String parseStringField(JsonNode node, String field) {
+ return parseString(node.get(field));
+ }
+
+ private Iterable parseStringArray(JsonNode node) {
+ List result = new ArrayList();
+
+ for (JsonNode element : node) {
+ result.add(parseString(element));
+ }
+
+ return result;
+ }
+
+ private String parseString(JsonNode node) {
+ return node.asText();
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchSpecificRevisionHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchSpecificRevisionHandler.java
new file mode 100644
index 0000000..0696c8d
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchSpecificRevisionHandler.java
@@ -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");
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PostBinaryHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PostBinaryHandler.java
new file mode 100644
index 0000000..37ec159
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PostBinaryHandler.java
@@ -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();
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/RemoteValues.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/RemoteValues.java
new file mode 100644
index 0000000..6c90500
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/RemoteValues.java
@@ -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> getBinaryWriter() {
+ return new GeneratorWriter>() {
+
+ @Override
+ public void write(JsonGenerator generator, RemoteValue.Supplier value) throws IOException {
+ generator.writeString(BaseEncoding.base64().encode(ByteStreams.toByteArray(value.get())));
+ }
+
+ };
+ }
+
+ private static GeneratorWriter getStringWriter() {
+ return new GeneratorWriter() {
+
+ @Override
+ public void write(JsonGenerator generator, String value) throws IOException {
+ generator.writeString(value);
+ }
+
+ };
+ }
+
+ private static GeneratorWriter getBooleanWriter() {
+ return new GeneratorWriter() {
+
+ @Override
+ public void write(JsonGenerator generator, Boolean value) throws IOException {
+ generator.writeBoolean(value);
+ }
+
+ };
+ }
+
+ private static GeneratorWriter getLongWriter() {
+ return new GeneratorWriter() {
+
+ @Override
+ public void write(JsonGenerator generator, Long value) throws IOException {
+ generator.writeNumber(value);
+ }
+
+ };
+ }
+
+ private static GeneratorWriter getDecimalWriter() {
+ return new GeneratorWriter() {
+
+ @Override
+ public void write(JsonGenerator generator, BigDecimal value) throws IOException {
+ generator.writeString(value.toString());
+ }
+
+ };
+ }
+
+ private static GeneratorWriter getDoubleWriter() {
+ return new GeneratorWriter() {
+
+ @Override
+ public void write(JsonGenerator generator, Double value) throws IOException {
+ generator.writeNumber(value);
+ }
+
+ };
+ }
+
+ private static void renderValue(JsonGenerator generator, String type, T value, GeneratorWriter writer) throws IOException {
+ generator.writeStartObject();
+ generator.writeStringField("type", type);
+ generator.writeFieldName("value");
+
+ writer.write(generator, value);
+
+ generator.writeEndObject();
+ }
+
+ private static void renderMultiValue(JsonGenerator generator, String type, Iterable values, GeneratorWriter 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 {
+
+ void write(JsonGenerator generator, T value) throws IOException;
+
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/ResponseUtils.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/ResponseUtils.java
new file mode 100644
index 0000000..603af40
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/ResponseUtils.java
@@ -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(), message);
+ }
+
+ private static void send(HttpServletResponse response, int code, Map headers, String message) throws IOException {
+ response.setStatus(code);
+ response.setContentType("application/json");
+
+ for (Map.Entry 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 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 headers, String message) throws IOException {
+ send(response, HttpServletResponse.SC_UNAUTHORIZED, headers, message);
+ }
+
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchLastRevisionHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchLastRevisionHandler.java
new file mode 100644
index 0000000..ddabb87
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchLastRevisionHandler.java
@@ -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();
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchRevisionHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchRevisionHandler.java
new file mode 100644
index 0000000..8636dba
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchRevisionHandler.java
@@ -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 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();
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchSpecificRevisionHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchSpecificRevisionHandler.java
new file mode 100644
index 0000000..208c12f
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchSpecificRevisionHandler.java
@@ -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");
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/UnauthorizedHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/UnauthorizedHandler.java
new file mode 100644
index 0000000..a04145c
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/UnauthorizedHandler.java
@@ -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");
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/AllMatcher.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/AllMatcher.java
new file mode 100644
index 0000000..aebb192
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/AllMatcher.java
@@ -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;
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matcher.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matcher.java
new file mode 100644
index 0000000..a551833
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matcher.java
@@ -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);
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matchers.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matchers.java
new file mode 100644
index 0000000..c609d22
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matchers.java
@@ -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));
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/MethodMatcher.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/MethodMatcher.java
new file mode 100644
index 0000000..6ce44ff
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/MethodMatcher.java
@@ -0,0 +1,35 @@
+/*
+ * 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 MethodMatcher implements Matcher {
+
+ private final String method;
+
+ public MethodMatcher(String method) {
+ this.method = method;
+ }
+
+ @Override
+ public boolean match(HttpServletRequest request) {
+ return request.getMethod().toLowerCase().equals(method.toLowerCase());
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/PathMatcher.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/PathMatcher.java
new file mode 100644
index 0000000..d8d04af
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/PathMatcher.java
@@ -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.matcher;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.regex.Pattern;
+
+class PathMatcher implements Matcher {
+
+ private final Pattern pattern;
+
+ public PathMatcher(Pattern pattern) {
+ this.pattern = pattern;
+ }
+
+ @Override
+ public boolean match(HttpServletRequest request) {
+ String requestPath = request.getPathInfo();
+
+ if (requestPath == null) {
+ return false;
+ }
+
+ return pattern.matcher(requestPath).matches();
+ }
+
+}
diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/osgi/RemoteServletRegistration.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/osgi/RemoteServletRegistration.java
new file mode 100644
index 0000000..39e64fd
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/osgi/RemoteServletRegistration.java
@@ -0,0 +1,92 @@
+/*
+ * 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.osgi;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Properties;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.jackrabbit.oak.api.ContentRepository;
+import org.apache.jackrabbit.oak.remote.RemoteRepository;
+import org.apache.jackrabbit.oak.remote.content.ContentRemoteRepository;
+import org.apache.jackrabbit.oak.remote.http.RemoteServlet;
+import org.osgi.service.http.HttpService;
+
+import java.util.Map;
+
+import static org.apache.felix.scr.annotations.ConfigurationPolicy.REQUIRE;
+
+@Component(
+ metatype = true,
+ policy = REQUIRE,
+ label = "Apache Jackrabbit Oak Remote HTTP API",
+ description = "The HTTP binding of the Remote API for a Jackrabbit Oak repository"
+)
+@Properties({
+ @Property(
+ name = "url",
+ value = {"/api"},
+ label = "Mount URL",
+ description = "Where the root application is exposed in the URL namespace"
+ )
+})
+public class RemoteServletRegistration {
+
+ @Reference
+ private HttpService httpService;
+
+ @Reference
+ private ContentRepository contentRepository;
+
+ @Activate
+ public void activate(Map properties) {
+ registerServlet(getUrl(properties));
+ }
+
+ @Deactivate
+ public void deactivate(Map properties) {
+ unregisterServlet(getUrl(properties));
+ }
+
+ private void registerServlet(String url) {
+ try {
+ httpService.registerServlet(url, getRemoteServlet(), null, null);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void unregisterServlet(String url) {
+ httpService.unregister(url);
+ }
+
+ private String getUrl(Map properties) {
+ return (String) properties.get("url");
+ }
+
+ private RemoteServlet getRemoteServlet() {
+ return new RemoteServlet(getRemoteRepository());
+ }
+
+ private RemoteRepository getRemoteRepository() {
+ return new ContentRemoteRepository(contentRepository);
+ }
+
+}
diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/AddContentRemoteOperationTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/AddContentRemoteOperationTest.java
new file mode 100644
index 0000000..6ab5c75
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/AddContentRemoteOperationTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.junit.Test;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class AddContentRemoteOperationTest {
+
+ public AddContentRemoteOperation createOperation(String path) {
+ return new AddContentRemoteOperation(path);
+ }
+
+ @Test
+ public void testAddNode() throws Exception {
+ Tree parent = mock(Tree.class);
+ doReturn(true).when(parent).exists();
+
+ Tree tree = mock(Tree.class);
+ doReturn(false).when(tree).exists();
+ doReturn(parent).when(tree).getParent();
+ doReturn("test").when(tree).getName();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test").apply(root);
+
+ verify(parent).addChild("test");
+ }
+
+ @Test(expected = RemoteCommitException.class)
+ public void testAddNodeWithExistingTree() throws Exception {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test").apply(root);
+ }
+
+ @Test(expected = RemoteCommitException.class)
+ public void testAddNodeWithNonExistingParent() throws Exception {
+ Tree parent = mock(Tree.class);
+ doReturn(false).when(parent).exists();
+
+ Tree tree = mock(Tree.class);
+ doReturn(false).when(tree).exists();
+ doReturn(parent).when(tree).getParent();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test").apply(root);
+ }
+
+}
diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRepositoryTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRepositoryTest.java
new file mode 100644
index 0000000..e243841
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRepositoryTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.collect.Sets;
+import org.apache.jackrabbit.oak.api.ContentRepository;
+import org.apache.jackrabbit.oak.api.ContentSession;
+import org.apache.jackrabbit.oak.remote.RemoteCredentials;
+import org.apache.jackrabbit.oak.remote.RemoteLoginException;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import javax.jcr.Credentials;
+import javax.security.auth.login.LoginException;
+
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ContentRemoteRepositoryTest {
+
+ private ContentRemoteRepository createRepository() {
+ return createRepository(mock(ContentRepository.class));
+ }
+
+ private ContentRemoteRepository createRepository(ContentRepository repository) {
+ return new ContentRemoteRepository(repository);
+ }
+
+ @Test
+ public void testCreateBasicCredentials() {
+ assertNotNull(createRepository().createBasicCredentials("admin", "admin".toCharArray()));
+ }
+
+ @Test
+ @Ignore
+ public void testCreateImpersonationCredentials() {
+ assertNotNull(createRepository().createImpersonationCredentials(Sets.newHashSet("admin")));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testLoginWithNullCredentials() throws Exception {
+ createRepository().login(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testLoginWithInvalidCredentials() throws Exception {
+ createRepository().login(mock(RemoteCredentials.class));
+ }
+
+ @Test
+ public void testSuccessfulLoginWithBasicCredentials() throws Exception {
+ ContentRepository repository = mock(ContentRepository.class);
+ when(repository.login(any(Credentials.class), anyString())).thenReturn(mock(ContentSession.class));
+
+ ContentRemoteRepository remoteRepository = createRepository(repository);
+ assertNotNull(remoteRepository.login(remoteRepository.createBasicCredentials("admin", "admin".toCharArray())));
+ }
+
+ @Test(expected = RemoteLoginException.class)
+ public void testUnsuccessfulLoginWithBasicCredentials() throws Exception {
+ ContentRepository repository = mock(ContentRepository.class);
+ when(repository.login(any(Credentials.class), anyString())).thenThrow(LoginException.class);
+
+ ContentRemoteRepository remoteRepository = createRepository(repository);
+ remoteRepository.login(remoteRepository.createBasicCredentials("admin", "admin".toCharArray()));
+ }
+
+}
diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResultTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResultTest.java
new file mode 100644
index 0000000..4900eee
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResultTest.java
@@ -0,0 +1,449 @@
+/*
+ * 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.PropertyValue;
+import org.apache.jackrabbit.oak.api.ResultRow;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.remote.RemoteValue;
+import org.apache.jackrabbit.util.ISO8601;
+import org.junit.Test;
+
+import java.math.BigDecimal;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+
+public class ContentRemoteResultTest {
+
+ private ContentRemoteResult createResult(ResultRow row) {
+ return new ContentRemoteResult(mock(ContentRemoteBinaries.class), row);
+ }
+
+ private ContentRemoteResult createResult(ContentRemoteBinaries binaries, ResultRow row) {
+ return new ContentRemoteResult(binaries, row);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testColumnNotAvailable() {
+ ResultRow row = mock(ResultRow.class);
+ doThrow(IllegalArgumentException.class).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ result.getColumnValue("column");
+ }
+
+ @Test
+ public void testStringColumn() {
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.STRING).when(value).getType();
+ doReturn("value").when(value).getValue(Type.STRING);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals("value", remoteValue.asText());
+ }
+
+ @Test
+ public void testBinaryColumn() {
+ Blob blob = mock(Blob.class);
+
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.BINARY).when(value).getType();
+ doReturn(blob).when(value).getValue(Type.BINARY);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteBinaries binaries = mock(ContentRemoteBinaries.class);
+ doReturn("id").when(binaries).put(blob);
+
+ ContentRemoteResult result = createResult(binaries, row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals("id", remoteValue.asBinaryId());
+ }
+
+ @Test
+ public void testLongColumn() {
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.LONG).when(value).getType();
+ doReturn(42L).when(value).getValue(Type.LONG);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals(42L, remoteValue.asLong().longValue());
+ }
+
+ @Test
+ public void testDoubleColumn() {
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.DOUBLE).when(value).getType();
+ doReturn(4.2).when(value).getValue(Type.DOUBLE);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals(4.2, remoteValue.asDouble().doubleValue(), 1e-5);
+ }
+
+ @Test
+ public void testDateColumn() {
+ Date now = new Date();
+
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.DATE).when(value).getType();
+ doReturn(toFormattedDate(now)).when(value).getValue(Type.DATE);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals(now.getTime(), remoteValue.asDate().longValue());
+ }
+
+ @Test
+ public void testBooleanColumn() {
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.BOOLEAN).when(value).getType();
+ doReturn(true).when(value).getValue(Type.BOOLEAN);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals(true, remoteValue.asBoolean());
+ }
+
+ @Test
+ public void testNameColumn() {
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.NAME).when(value).getType();
+ doReturn("value").when(value).getValue(Type.NAME);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals("value", remoteValue.asName());
+ }
+
+ @Test
+ public void testPathColumn() {
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.PATH).when(value).getType();
+ doReturn("value").when(value).getValue(Type.PATH);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals("value", remoteValue.asPath());
+ }
+
+ @Test
+ public void testReferenceColumn() {
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.REFERENCE).when(value).getType();
+ doReturn("value").when(value).getValue(Type.REFERENCE);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals("value", remoteValue.asReference());
+ }
+
+ @Test
+ public void testWeakReferenceColumn() {
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.WEAKREFERENCE).when(value).getType();
+ doReturn("value").when(value).getValue(Type.WEAKREFERENCE);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals("value", remoteValue.asWeakReference());
+ }
+
+ @Test
+ public void testUriColumn() {
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.URI).when(value).getType();
+ doReturn("value").when(value).getValue(Type.URI);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals("value", remoteValue.asUri());
+ }
+
+ @Test
+ public void testDecimalColumn() {
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.DECIMAL).when(value).getType();
+ doReturn(BigDecimal.ONE).when(value).getValue(Type.DECIMAL);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals(BigDecimal.ONE, remoteValue.asDecimal());
+ }
+
+ @Test
+ public void testMultiStringColumn() {
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.STRINGS).when(value).getType();
+ doReturn(asList("a", "b")).when(value).getValue(Type.STRINGS);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals(asList("a", "b"), remoteValue.asMultiText());
+ }
+
+ @Test
+ public void testMultiBinaryColumn() {
+ Blob first = mock(Blob.class);
+ Blob second = mock(Blob.class);
+
+ ContentRemoteBinaries binaries = mock(ContentRemoteBinaries.class);
+ doReturn("first").when(binaries).put(first);
+ doReturn("second").when(binaries).put(second);
+
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.BINARIES).when(value).getType();
+ doReturn(asList(first, second)).when(value).getValue(Type.BINARIES);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(binaries, row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals(asList("first", "second"), remoteValue.asMultiBinaryId());
+ }
+
+ @Test
+ public void testMultiLongColumn() {
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.LONGS).when(value).getType();
+ doReturn(asList(4L, 2L)).when(value).getValue(Type.LONGS);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals(asList(4L, 2L), remoteValue.asMultiLong());
+ }
+
+ @Test
+ public void testMultiDoubleColumn() {
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.DOUBLES).when(value).getType();
+ doReturn(asList(4.0, 2.0)).when(value).getValue(Type.DOUBLES);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals(asList(4.0, 2.0), remoteValue.asMultiDouble());
+ }
+
+ @Test
+ public void testMultiDateColumn() {
+ Date first = new Date(4);
+ Date second = new Date(2);
+
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.DATES).when(value).getType();
+ doReturn(toFormattedDates(first, second)).when(value).getValue(Type.DATES);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals(asList(4L, 2L), remoteValue.asMultiDate());
+ }
+
+ @Test
+ public void testMultiBooleanColumn() {
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.BOOLEANS).when(value).getType();
+ doReturn(asList(true, false)).when(value).getValue(Type.BOOLEANS);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals(asList(true, false), remoteValue.asMultiBoolean());
+ }
+
+ @Test
+ public void testMultiNameColumn() {
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.NAMES).when(value).getType();
+ doReturn(asList("a", "b")).when(value).getValue(Type.NAMES);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals(asList("a", "b"), remoteValue.asMultiName());
+ }
+
+ @Test
+ public void testMultiPathColumn() {
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.PATHS).when(value).getType();
+ doReturn(asList("a", "b")).when(value).getValue(Type.PATHS);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals(asList("a", "b"), remoteValue.asMultiPath());
+ }
+
+ @Test
+ public void testMultiReferenceColumn() {
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.REFERENCES).when(value).getType();
+ doReturn(asList("a", "b")).when(value).getValue(Type.REFERENCES);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals(asList("a", "b"), remoteValue.asMultiReference());
+ }
+
+ @Test
+ public void testMultiWeakReferenceColumn() {
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.WEAKREFERENCES).when(value).getType();
+ doReturn(asList("a", "b")).when(value).getValue(Type.WEAKREFERENCES);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals(asList("a", "b"), remoteValue.asMultiWeakReference());
+ }
+
+ @Test
+ public void testMultiUriColumn() {
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.URIS).when(value).getType();
+ doReturn(asList("a", "b")).when(value).getValue(Type.URIS);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals(asList("a", "b"), remoteValue.asMultiUri());
+ }
+
+ @Test
+ public void testMultiDecimalColumn() {
+ BigDecimal first = new BigDecimal(4);
+ BigDecimal second = new BigDecimal(2);
+
+ PropertyValue value = mock(PropertyValue.class);
+ doReturn(Type.DECIMALS).when(value).getType();
+ doReturn(asList(first, second)).when(value).getValue(Type.DECIMALS);
+
+ ResultRow row = mock(ResultRow.class);
+ doReturn(value).when(row).getValue("column");
+
+ ContentRemoteResult result = createResult(row);
+ RemoteValue remoteValue = result.getColumnValue("column");
+ assertEquals(asList(first, second), remoteValue.asMultiDecimal());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSelectorNotAvailable() {
+ ResultRow row = mock(ResultRow.class);
+ doThrow(IllegalArgumentException.class).when(row).getPath("selector");
+ createResult(row).getSelectorPath("selector");
+ }
+
+ @Test
+ public void testSelector() {
+ ResultRow row = mock(ResultRow.class);
+ doReturn("path").when(row).getPath("selector");
+ assertEquals("path", createResult(row).getSelectorPath("selector"));
+ }
+
+ private Iterable toFormattedDates(Date... dates) {
+ List result = newArrayList();
+
+ for (Date date : dates) {
+ result.add(toFormattedDate(date));
+ }
+
+ return result;
+ }
+
+ private String toFormattedDate(Date date) {
+ return ISO8601.format(toCalendar(date));
+ }
+
+ private Calendar toCalendar(Date date) {
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(date.getTime());
+ return calendar;
+ }
+
+}
diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResultsTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResultsTest.java
new file mode 100644
index 0000000..b54034c
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResultsTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.Result;
+import org.junit.Test;
+
+import static com.google.common.collect.Iterables.elementsEqual;
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+public class ContentRemoteResultsTest {
+
+ private ContentRemoteResults createContentRemoteResults(Result result) {
+ return new ContentRemoteResults(createContentRemoteBinaries(), result);
+ }
+
+ private ContentRemoteBinaries createContentRemoteBinaries() {
+ return mock(ContentRemoteBinaries.class);
+ }
+
+ @Test
+ public void testGetTotal() {
+ Result result = mock(Result.class);
+ doReturn(42L).when(result).getSize();
+ assertEquals(42L, createContentRemoteResults(result).getTotal());
+ }
+
+ @Test
+ public void testGetColumns() {
+ String[] columns = {"a", "b"};
+
+ Result result = mock(Result.class);
+ doReturn(columns).when(result).getColumnNames();
+
+ assertTrue(elementsEqual(asList(columns), createContentRemoteResults(result).getColumns()));
+ }
+
+ @Test
+ public void testGetSelectors() {
+ String[] selectors = {"a", "b"};
+
+ Result result = mock(Result.class);
+ doReturn(selectors).when(result).getSelectorNames();
+
+ assertTrue(elementsEqual(asList(selectors), createContentRemoteResults(result).getSelectors()));
+ }
+
+}
diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteSessionTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteSessionTest.java
new file mode 100644
index 0000000..8a2ee81
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteSessionTest.java
@@ -0,0 +1,542 @@
+/*
+ * 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.AuthInfo;
+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.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.RemoteRevision;
+import org.apache.jackrabbit.oak.remote.RemoteTreeFilters;
+import org.apache.jackrabbit.oak.remote.RemoteValue;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ContentRemoteSessionTest {
+
+ private ContentRemoteSession createSession() {
+ return createSession(mock(ContentSession.class));
+ }
+
+ private ContentRemoteSession createSession(ContentRemoteBinaries binaries) {
+ return new ContentRemoteSession(mock(ContentSession.class), mock(ContentRemoteRevisions.class), binaries);
+ }
+
+ private ContentRemoteSession createSession(ContentSession session, ContentRemoteBinaries binaries) {
+ return new ContentRemoteSession(session, mock(ContentRemoteRevisions.class), binaries);
+ }
+
+ private ContentRemoteSession createSession(ContentSession session, ContentRemoteRevisions revisions) {
+ return new ContentRemoteSession(session, revisions, mock(ContentRemoteBinaries.class));
+ }
+
+ private ContentRemoteSession createSession(ContentSession session) {
+ return new ContentRemoteSession(session, mock(ContentRemoteRevisions.class), mock(ContentRemoteBinaries.class));
+ }
+
+ @Test
+ public void testReadLastRevision() {
+ assertNotNull(createSession().readLastRevision());
+ }
+
+ @Test
+ public void testReadLastRevisionAsString() {
+ Root root = mock(Root.class);
+
+ AuthInfo authInfo = mock(AuthInfo.class);
+
+ ContentSession session = mock(ContentSession.class);
+ doReturn(authInfo).when(session).getAuthInfo();
+ doReturn(root).when(session).getLatestRoot();
+
+ ContentRemoteRevisions revisions = mock(ContentRemoteRevisions.class);
+ doReturn("id").when(revisions).put(authInfo, root);
+
+ assertEquals("id", createSession(session, revisions).readLastRevision().asString());
+ }
+
+ @Test
+ public void testReadRevision() {
+ Root root = mock(Root.class);
+
+ AuthInfo authInfo = mock(AuthInfo.class);
+
+ ContentSession session = mock(ContentSession.class);
+ doReturn(authInfo).when(session).getAuthInfo();
+
+ ContentRemoteRevisions revisions = mock(ContentRemoteRevisions.class);
+ doReturn(root).when(revisions).get(authInfo, "id");
+
+ assertNotNull(createSession(session, revisions).readRevision("id"));
+ }
+
+ @Test
+ public void testReadRevisionAsString() {
+ Root root = mock(Root.class);
+
+ AuthInfo authInfo = mock(AuthInfo.class);
+
+ ContentSession session = mock(ContentSession.class);
+ doReturn(authInfo).when(session).getAuthInfo();
+
+ ContentRemoteRevisions revisions = mock(ContentRemoteRevisions.class);
+ doReturn(root).when(revisions).get(authInfo, "id");
+
+ assertEquals("id", createSession(session, revisions).readRevision("id").asString());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testReadTreeWithNullRevision() throws Exception {
+ createSession().readTree(null, "/", new RemoteTreeFilters());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testReadTreeWithInvalidRevision() throws Exception {
+ createSession().readTree(mock(RemoteRevision.class), "/", new RemoteTreeFilters());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testReadTreeWithNullPath() throws Exception {
+ ContentRemoteRevision revision = mock(ContentRemoteRevision.class);
+ createSession().readTree(revision, null, new RemoteTreeFilters());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testReadTreeWithInvalidPath() throws Exception {
+ ContentRemoteRevision revision = mock(ContentRemoteRevision.class);
+ createSession().readTree(revision, "invalid", new RemoteTreeFilters());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testReadTreeWithNullFilters() throws Exception {
+ ContentRemoteRevision revision = mock(ContentRemoteRevision.class);
+ createSession().readTree(revision, "/", null);
+ }
+
+ @Test
+ public void testReadNonExistingTree() throws Exception {
+ Tree tree = mock(Tree.class);
+ when(tree.exists()).thenReturn(false);
+
+ Root root = mock(Root.class);
+ when(root.getTree(anyString())).thenReturn(tree);
+
+ ContentRemoteRevision revision = mock(ContentRemoteRevision.class);
+ doReturn(root).when(revision).getRoot();
+
+ assertNull(createSession().readTree(revision, "/", new RemoteTreeFilters()));
+ }
+
+ @Test
+ public void testReadExistingTree() throws Exception {
+ Tree tree = mock(Tree.class);
+ when(tree.exists()).thenReturn(true);
+
+ Root root = mock(Root.class);
+ when(root.getTree(anyString())).thenReturn(tree);
+
+ ContentRemoteRevision revision = mock(ContentRemoteRevision.class);
+ doReturn(root).when(revision).getRoot();
+
+ assertNotNull(createSession().readTree(revision, "/", new RemoteTreeFilters()));
+ }
+
+ @Test
+ public void testCreateAddOperation() {
+ assertNotNull(createSession().createAddOperation("/test", new HashMap()));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateAddOperationWithNullPath() {
+ createSession().createAddOperation(null, new HashMap());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateAddOperationWithInvalidPath() {
+ createSession().createAddOperation("invalid", new HashMap());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateAddOperationWithRootPath() {
+ createSession().createAddOperation("/", new HashMap());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void createAddOperationWithNullProperties() {
+ createSession().createAddOperation("/test", null);
+ }
+
+ @Test
+ public void testCreateRemoveOperation() {
+ assertNotNull(createSession().createRemoveOperation("/test"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateRemoveOperationWithNullPath() {
+ createSession().createRemoveOperation(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateRemoveOperationWithInvalidPath() {
+ createSession().createRemoveOperation("invalid");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateRemoveOperationWithRootPath() {
+ createSession().createRemoveOperation("/");
+ }
+
+ @Test
+ public void testCreateSetOperation() {
+ assertNotNull(createSession().createSetOperation("/test", "name", RemoteValue.toText("value")));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateSetOperationWithNullPath() {
+ createSession().createSetOperation(null, "name", RemoteValue.toText("value"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateSetOperationWithInvalidPath() {
+ createSession().createSetOperation("invalid", "name", RemoteValue.toText("value"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateSetOperationWithNullName() {
+ createSession().createSetOperation("/test", null, RemoteValue.toText("value"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateSetOperationWithEmptyName() {
+ createSession().createSetOperation("/test", "", RemoteValue.toText("value"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateSetOperationWithNullValue() {
+ createSession().createSetOperation("/test", "name", null);
+ }
+
+ @Test
+ public void testCreateUnsetOperation() {
+ assertNotNull(createSession().createUnsetOperation("/test", "name"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateUnsetOperationWithNullPath() {
+ createSession().createUnsetOperation(null, "name");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateUnsetOperationWithInvalidPath() {
+ createSession().createUnsetOperation("invalid", "name");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateUnsetOperationWithNullName() {
+ createSession().createUnsetOperation("/test", null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateUnsetOperationWithEmptyName() {
+ createSession().createUnsetOperation("/test", "");
+ }
+
+ @Test
+ public void createCopyOperation() {
+ assertNotNull(createSession().createCopyOperation("/source", "/target"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void createCopyOperationWithNullSourcePath() {
+ createSession().createCopyOperation(null, "/target");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void createCopyOperationWithInvalidSourcePath() {
+ createSession().createCopyOperation("invalid", "/target");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void createCopyOperationWithNullTargetPath() {
+ createSession().createCopyOperation("/source", null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void createCopyOperationWithInvalidTargetPath() {
+ createSession().createCopyOperation("/source", "invalid");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void createCopyOperationWithSameSourceAndTargetPath() {
+ createSession().createCopyOperation("/same", "/same");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void createCopyOperationWithSourceAncestorOfTarget() {
+ createSession().createCopyOperation("/source", "/source/target");
+ }
+
+ @Test
+ public void createMoveOperation() {
+ assertNotNull(createSession().createMoveOperation("/source", "/target"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void createMoveOperationWithNullSourcePath() {
+ createSession().createMoveOperation(null, "/target");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void createMoveOperationWithInvalidSourcePath() {
+ createSession().createMoveOperation("invalid", "/target");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void createMoveOperationWithNullTargetPath() {
+ createSession().createMoveOperation("/source", null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void createMoveOperationWithInvalidTargetPath() {
+ createSession().createMoveOperation("/source", "invalid");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void createMoveOperationWithSameSourceAndTargetPath() {
+ createSession().createMoveOperation("/same", "/same");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void createMoveOperationWithSourceAncestorOfTarget() {
+ createSession().createMoveOperation("/source", "/source/target");
+ }
+
+ @Test
+ public void createAggregateOperation() {
+ assertNotNull(createSession().createAggregateOperation(new ArrayList()));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void createAggregateOperationWithNullList() {
+ createSession().createAggregateOperation(null);
+ }
+
+ @Test
+ public void testCommit() throws Exception {
+ Root root = mock(Root.class);
+
+ ContentRemoteOperation operation = mock(ContentRemoteOperation.class);
+
+ ContentRemoteRevision revision = mock(ContentRemoteRevision.class);
+ doReturn(root).when(revision).getRoot();
+
+ assertNotNull(createSession().commit(revision, operation));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCommitWithNullRevision() throws Exception {
+ createSession().commit(null, mock(ContentRemoteOperation.class));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCommitWithInvalidRevision() throws Exception {
+ createSession().commit(mock(RemoteRevision.class), mock(ContentRemoteOperation.class));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCommitWithNullOperation() throws Exception {
+ createSession().commit(mock(ContentRemoteRevision.class), null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCommitWithInvalidOperation() throws Exception {
+ createSession().commit(mock(ContentRemoteRevision.class), mock(RemoteOperation.class));
+ }
+
+ @Test(expected = RemoteCommitException.class)
+ public void testCommitWithOperationThrowingException() throws Exception {
+ Root root = mock(Root.class);
+
+ ContentRemoteOperation operation = mock(ContentRemoteOperation.class);
+ doThrow(RemoteCommitException.class).when(operation).apply(root);
+
+ ContentRemoteRevision revision = mock(ContentRemoteRevision.class);
+ doReturn(root).when(revision).getRoot();
+
+ createSession().commit(revision, operation);
+ }
+
+ @Test(expected = RemoteCommitException.class)
+ public void testCommitWithConflictingCommit() throws Exception {
+ Root root = mock(Root.class);
+ doThrow(CommitFailedException.class).when(root).commit();
+
+ ContentRemoteRevision revision = mock(ContentRemoteRevision.class);
+ doReturn(root).when(revision).getRoot();
+
+ createSession().commit(revision, mock(ContentRemoteOperation.class));
+ }
+
+ @Test
+ public void testReadBinaryId() {
+ Blob blob = mock(Blob.class);
+
+ ContentRemoteBinaries binaries = mock(ContentRemoteBinaries.class);
+ doReturn(blob).when(binaries).get("id");
+
+ ContentRemoteSession remoteSession = createSession(binaries);
+ assertNotNull(remoteSession.readBinaryId("id"));
+ }
+
+ @Test
+ public void testReadBinaryIdAsString() {
+ Blob blob = mock(Blob.class);
+
+ ContentRemoteBinaries binaries = mock(ContentRemoteBinaries.class);
+ doReturn(blob).when(binaries).get("id");
+
+ ContentRemoteSession remoteSession = createSession(binaries);
+ assertEquals("id", remoteSession.readBinaryId("id").asString());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testReadBinaryIdWithNullReference() {
+ createSession().readBinaryId(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testReadBinaryIdWithEmptyReference() {
+ createSession().readBinaryId("");
+ }
+
+ @Test
+ public void testReadBinaryIdWithInvalidReference() {
+ Root root = mock(Root.class);
+ doReturn(null).when(root).getBlob(anyString());
+
+ ContentSession session = mock(ContentSession.class);
+ doReturn(root).when(session).getLatestRoot();
+
+ ContentRemoteSession remoteSession = createSession(session);
+ assertNull(remoteSession.readBinaryId("id"));
+ }
+
+ @Test
+ public void testReadBinary() {
+ Blob blob = mock(Blob.class);
+
+ ContentRemoteBinaryId binaryId = mock(ContentRemoteBinaryId.class);
+ doReturn(blob).when(binaryId).asBlob();
+
+ assertNotNull(createSession().readBinary(binaryId, new RemoteBinaryFilters()));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testReadBinaryWithNullId() {
+ createSession().readBinary(null, new RemoteBinaryFilters());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testReadBinaryWithInvalidId() {
+ createSession().readBinary(mock(RemoteBinaryId.class), new RemoteBinaryFilters());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testReadBinaryWithNullFilters() {
+ createSession().readBinary(mock(ContentRemoteBinaryId.class), null);
+ }
+
+ @Test
+ public void testWriteBinary() throws Exception {
+ Blob blob = mock(Blob.class);
+
+ InputStream stream = mock(InputStream.class);
+
+ Root root = mock(Root.class);
+ doReturn(blob).when(root).createBlob(stream);
+
+ ContentSession session = mock(ContentSession.class);
+ doReturn(root).when(session).getLatestRoot();
+
+ ContentRemoteBinaries binaries = mock(ContentRemoteBinaries.class);
+ doReturn("id").when(binaries).put(blob);
+
+ ContentRemoteSession remoteSession = createSession(session, binaries);
+ assertEquals("id", remoteSession.writeBinary(stream).asString());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testWriteBinaryWithNullStream() throws Exception {
+ createSession().writeBinary(null);
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testWriteBinaryFailure() throws Exception {
+ InputStream stream = mock(InputStream.class);
+
+ Root root = mock(Root.class);
+ doThrow(IOException.class).when(root).createBlob(stream);
+
+ ContentSession session = mock(ContentSession.class);
+ doReturn(root).when(session).getLatestRoot();
+
+ ContentRemoteSession remoteSession = createSession(session);
+ remoteSession.writeBinary(stream);
+ }
+
+ @Test
+ public void testReadBinaryLength() throws Exception {
+ Blob blob = mock(Blob.class);
+ doReturn(42L).when(blob).length();
+
+ ContentRemoteBinaryId binaryId = mock(ContentRemoteBinaryId.class);
+ doReturn(blob).when(binaryId).asBlob();
+
+ assertEquals(42L, createSession().readBinaryLength(binaryId));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testReadBinaryLengthWithNullBinaryId() throws Exception {
+ createSession().readBinaryLength(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testReadBinaryLengthWithInvalidBinaryId() throws Exception {
+ createSession().readBinaryLength(mock(RemoteBinaryId.class));
+ }
+
+}
diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteTreeTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteTreeTest.java
new file mode 100644
index 0000000..027fbec
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteTreeTest.java
@@ -0,0 +1,947 @@
+/*
+ * 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.collect.Sets;
+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.util.ISO8601;
+import org.junit.Test;
+
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.util.Calendar;
+import java.util.Map;
+import java.util.Set;
+
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.common.collect.Sets.newHashSet;
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+public class ContentRemoteTreeTest {
+
+ private ContentRemoteTree createTree(Tree tree) {
+ return new ContentRemoteTree(tree, 0, new RemoteTreeFilters(), mock(ContentRemoteBinaries.class));
+ }
+
+ private ContentRemoteTree createTree(Tree tree, ContentRemoteBinaries binaries) {
+ return new ContentRemoteTree(tree, 0, new RemoteTreeFilters(), binaries);
+ }
+
+ private ContentRemoteTree createTree(Tree tree, RemoteTreeFilters filters) {
+ return new ContentRemoteTree(tree, 0, filters, mock(ContentRemoteBinaries.class));
+ }
+
+ @Test
+ public void testGetBinaryProperty() {
+ InputStream stream = mock(InputStream.class);
+
+ Blob blob = mock(Blob.class);
+ doReturn(stream).when(blob).getNewStream();
+
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.BINARY).when(property).getType();
+ doReturn(blob).when(property).getValue(Type.BINARY);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ ContentRemoteTree remoteTree = createTree(tree, new RemoteTreeFilters() {
+
+ @Override
+ public long getBinaryThreshold() {
+ return Long.MAX_VALUE;
+ }
+
+ });
+
+ Map properties = remoteTree.getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isBinary());
+ assertEquals(stream, properties.get("name").asBinary().get());
+ }
+
+ @Test
+ public void testGetMultiBinaryProperty() {
+ InputStream stream = mock(InputStream.class);
+
+ Blob blob = mock(Blob.class);
+ doReturn(stream).when(blob).getNewStream();
+
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.BINARIES).when(property).getType();
+ doReturn(singletonList(blob)).when(property).getValue(Type.BINARIES);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ ContentRemoteTree remoteTree = createTree(tree, new RemoteTreeFilters() {
+
+ @Override
+ public long getBinaryThreshold() {
+ return Long.MAX_VALUE;
+ }
+
+ });
+
+ Map properties = remoteTree.getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isMultiBinary());
+ assertEquals(stream, getOnlyElement(properties.get("name").asMultiBinary()).get());
+ }
+
+ @Test
+ public void testGetBinaryIdProperty() {
+ Blob blob = mock(Blob.class);
+
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.BINARY).when(property).getType();
+ doReturn(blob).when(property).getValue(Type.BINARY);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ ContentRemoteBinaries binaries = mock(ContentRemoteBinaries.class);
+ doReturn("id").when(binaries).put(blob);
+
+ Map properties = createTree(tree, binaries).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isBinaryId());
+ assertEquals("id", properties.get("name").asBinaryId());
+ }
+
+ @Test
+ public void testGetMultiBinaryIdProperty() {
+ Blob blob = mock(Blob.class);
+
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.BINARIES).when(property).getType();
+ doReturn(singletonList(blob)).when(property).getValue(Type.BINARIES);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ ContentRemoteBinaries binaries = mock(ContentRemoteBinaries.class);
+ doReturn("id").when(binaries).put(blob);
+
+ Map properties = createTree(tree, binaries).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isMultiBinaryId());
+ assertEquals("id", getOnlyElement(properties.get("name").asMultiBinaryId()));
+ }
+
+ @Test
+ public void testGetBooleanProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.BOOLEAN).when(property).getType();
+ doReturn(true).when(property).getValue(Type.BOOLEAN);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isBoolean());
+ assertEquals(true, properties.get("name").asBoolean());
+ }
+
+ @Test
+ public void testGetMultiBooleanProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.BOOLEANS).when(property).getType();
+ doReturn(singletonList(true)).when(property).getValue(Type.BOOLEANS);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isMultiBoolean());
+ assertEquals(true, getOnlyElement(properties.get("name").asMultiBoolean()));
+ }
+
+ @Test
+ public void testGetDateProperty() {
+ Calendar calendar = Calendar.getInstance();
+
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.DATE).when(property).getType();
+ doReturn(ISO8601.format(calendar)).when(property).getValue(Type.DATE);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isDate());
+ assertEquals(calendar.getTimeInMillis(), properties.get("name").asDate().longValue());
+ }
+
+ @Test
+ public void testGetMultiDateProperty() {
+ Calendar calendar = Calendar.getInstance();
+
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.DATES).when(property).getType();
+ doReturn(singletonList(ISO8601.format(calendar))).when(property).getValue(Type.DATES);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isMultiDate());
+ assertEquals(calendar.getTimeInMillis(), getOnlyElement(properties.get("name").asMultiDate()).longValue());
+ }
+
+ @Test
+ public void testDecimalProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.DECIMAL).when(property).getType();
+ doReturn(BigDecimal.ONE).when(property).getValue(Type.DECIMAL);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isDecimal());
+ assertEquals(BigDecimal.ONE, properties.get("name").asDecimal());
+ }
+
+ @Test
+ public void testGetMultiDecimalProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.DECIMALS).when(property).getType();
+ doReturn(singletonList(BigDecimal.ONE)).when(property).getValue(Type.DECIMALS);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isMultiDecimal());
+ assertEquals(BigDecimal.ONE, getOnlyElement(properties.get("name").asMultiDecimal()));
+ }
+
+ @Test
+ public void testDoubleProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.DOUBLE).when(property).getType();
+ doReturn(4.2).when(property).getValue(Type.DOUBLE);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isDouble());
+ assertEquals(4.2, properties.get("name").asDouble(), 1e-9);
+ }
+
+ @Test
+ public void testGetMultiDoubleProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.DOUBLES).when(property).getType();
+ doReturn(singletonList(4.2)).when(property).getValue(Type.DOUBLES);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isMultiDouble());
+ assertEquals(4.2, getOnlyElement(properties.get("name").asMultiDouble()), 1e-9);
+ }
+
+ @Test
+ public void testLongProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.LONG).when(property).getType();
+ doReturn(42L).when(property).getValue(Type.LONG);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isLong());
+ assertEquals(42L, properties.get("name").asLong().longValue());
+ }
+
+ @Test
+ public void testGetMultiLongProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.LONGS).when(property).getType();
+ doReturn(singletonList(42L)).when(property).getValue(Type.LONGS);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isMultiLong());
+ assertEquals(42L, getOnlyElement(properties.get("name").asMultiLong()).longValue());
+ }
+
+ @Test
+ public void testNameProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.NAME).when(property).getType();
+ doReturn("value").when(property).getValue(Type.NAME);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isName());
+ assertEquals("value", properties.get("name").asName());
+ }
+
+ @Test
+ public void testGetMultiNameProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.NAMES).when(property).getType();
+ doReturn(singletonList("value")).when(property).getValue(Type.NAMES);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isMultiName());
+ assertEquals("value", getOnlyElement(properties.get("name").asMultiName()));
+ }
+
+ @Test
+ public void testPathProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.PATH).when(property).getType();
+ doReturn("value").when(property).getValue(Type.PATH);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map