diff --git oak-remote/pom.xml oak-remote/pom.xml
new file mode 100644
index 0000000..a321f0e
--- /dev/null
+++ oak-remote/pom.xml
@@ -0,0 +1,94 @@
+
+
+
+
+
+ 4.0.0
+
+
+ org.apache.jackrabbit
+ oak-parent
+ 1.1-SNAPSHOT
+ ../oak-parent/pom.xml
+
+
+ oak-remote
+ Oak Remote API
+
+
+
+ com.google.guava
+ guava
+
+
+ javax.jcr
+ jcr
+ 2.0
+
+
+ org.apache.jackrabbit
+ oak-core
+ 1.1-SNAPSHOT
+
+
+ org.apache.jackrabbit
+ oak-commons
+ 1.1-SNAPSHOT
+
+
+ org.apache.jackrabbit
+ jackrabbit-jcr-commons
+ 2.9.0
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ 2.0.0
+
+
+ org.eclipse.jetty
+ jetty-server
+ 8.1.10.v20130312
+
+
+ org.eclipse.jetty
+ jetty-servlet
+ 8.1.10.v20130312
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ org.hamcrest
+ hamcrest-core
+ 1.3
+ test
+
+
+ org.mockito
+ mockito-all
+ 1.10.19
+ test
+
+
+
\ 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/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/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/RemoteRepository.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteRepository.java
new file mode 100644
index 0000000..a1d539f
--- /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.
+ * This method can return {@code null} if it's not possible to authenticate
+ * the provided credentials.
+ */
+ RemoteSession login(RemoteCredentials credentials);
+
+}
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..1bd792a
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteSession.java
@@ -0,0 +1,202 @@
+/*
+ * 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. This method can return {@code null} if the
+ * operations can't be applied because of conflicting changes.
+ */
+ RemoteRevision commit(RemoteRevision revision, RemoteOperation operation);
+
+ /**
+ * 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.
+ * This method can return {@code null} if the binary object referenced by
+ * the provided binary ID doesn't exist.
+ */
+ InputStream readBinary(RemoteBinaryId binaryId, RemoteBinaryFilters filters);
+
+ /**
+ * 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. This method may return {@code null} if it was not possible to
+ * write the binary object into the repository.
+ */
+ RemoteBinaryId writeBinary(InputStream stream);
+
+}
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..6a16b7c
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/AddContentRemoteOperation.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+class AddContentRemoteOperation implements ContentRemoteOperation {
+
+ private final String path;
+
+ public AddContentRemoteOperation(String path) {
+ this.path = path;
+ }
+
+ @Override
+ public void apply(Root root) {
+ Tree tree = root.getTree(path);
+
+ if (tree.exists()) {
+ throw new IllegalStateException("node already exists");
+ }
+
+ Tree parent = tree.getParent();
+
+ if (!parent.exists()) {
+ throw new IllegalStateException("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..e7fe349
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/AggregateContentRemoteOperation.java
@@ -0,0 +1,50 @@
+/*
+ * 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.RemoteOperation;
+
+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) {
+ for (RemoteOperation operation : operations) {
+ ContentRemoteOperation contentRemoteOperation = null;
+
+ if (operation instanceof ContentRemoteOperation) {
+ contentRemoteOperation = (ContentRemoteOperation) operation;
+ }
+
+ if (contentRemoteOperation == null) {
+ throw new IllegalStateException("invalid operation");
+ }
+
+ contentRemoteOperation.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..2e0d8a7
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/BasicContentRemoteCredentials.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.content;
+
+import org.apache.jackrabbit.oak.api.ContentRepository;
+import org.apache.jackrabbit.oak.api.ContentSession;
+
+import javax.jcr.SimpleCredentials;
+
+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) {
+ ContentSession session;
+
+ try {
+ session = repository.login(new SimpleCredentials(user, password), null);
+ } catch (Exception e) {
+ session = null;
+ }
+
+ return session;
+ }
+
+}
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..fe2d63f
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteCredentials.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.ContentRepository;
+import org.apache.jackrabbit.oak.api.ContentSession;
+import org.apache.jackrabbit.oak.remote.RemoteCredentials;
+
+interface ContentRemoteCredentials extends RemoteCredentials {
+
+ ContentSession login(ContentRepository repository);
+
+}
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..57b630a
--- /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(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..dd97a30
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteOperation.java
@@ -0,0 +1,27 @@
+/*
+ * 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.RemoteOperation;
+
+interface ContentRemoteOperation extends RemoteOperation {
+
+ void apply(Root root);
+
+}
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..d8284f0
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRepository.java
@@ -0,0 +1,155 @@
+/*
+ * 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.api.Root;
+import org.apache.jackrabbit.oak.remote.RemoteCredentials;
+import org.apache.jackrabbit.oak.remote.RemoteOperation;
+import org.apache.jackrabbit.oak.remote.RemoteRepository;
+import org.apache.jackrabbit.oak.remote.RemoteRevision;
+import org.apache.jackrabbit.oak.remote.RemoteSession;
+
+import java.util.Set;
+
+public class ContentRemoteRepository implements RemoteRepository {
+
+ private final ContentRepository contentRepository;
+
+ private final ContentRemoteRevisions contentRemoteRevisions;
+
+ public ContentRemoteRepository(ContentRepository contentRepository) {
+ this.contentRemoteRevisions = new ContentRemoteRevisions();
+ 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) {
+ ContentRemoteCredentials contentRemoteCredentials = null;
+
+ if (remoteCredentials instanceof ContentRemoteCredentials) {
+ contentRemoteCredentials = (ContentRemoteCredentials) remoteCredentials;
+ }
+
+ if (contentRemoteCredentials == null) {
+ throw new IllegalArgumentException("invalid credentials");
+ }
+
+ final ContentSession contentSession = contentRemoteCredentials.login(contentRepository);
+
+ if (contentSession == null) {
+ return null;
+ }
+
+ // TODO - remove this subclass of ContentRemoteSession as soon as the
+ // Oak API exposes a way to handle revisions/snapshots.
+
+ return new ContentRemoteSession(contentSession) {
+
+ @Override
+ public RemoteRevision readLastRevision() {
+ final Root root = contentSession.getLatestRoot();
+
+ final String revisionId = contentRemoteRevisions.put(contentSession.getAuthInfo(), root);
+
+ return new ContentRemoteRevision() {
+
+ @Override
+ public Root readRoot(ContentSession session) {
+ return root;
+ }
+
+ @Override
+ public String asString() {
+ return revisionId;
+ }
+
+ };
+ }
+
+ @Override
+ public RemoteRevision readRevision(final String revisionId) {
+ final Root root = contentRemoteRevisions.get(contentSession.getAuthInfo(), revisionId);
+
+ if (root == null) {
+ return null;
+ }
+
+ return new ContentRemoteRevision() {
+
+ @Override
+ public Root readRoot(ContentSession session) {
+ return root;
+ }
+
+ @Override
+ public String asString() {
+ return revisionId;
+ }
+
+ };
+ }
+
+ @Override
+ public RemoteRevision commit(RemoteRevision revision, RemoteOperation operation) {
+ ContentRemoteRevision contentRemoteRevision = null;
+
+ if (revision instanceof ContentRemoteRevision) {
+ contentRemoteRevision = (ContentRemoteRevision) revision;
+ }
+
+ if (contentRemoteRevision == null) {
+ throw new IllegalArgumentException("invalid revision");
+ }
+
+ final Root root = contentRemoteRevision.readRoot(contentSession);
+
+ super.commit(revision, operation);
+
+ final String revisionId = contentRemoteRevisions.put(contentSession.getAuthInfo(), root);
+
+ return new ContentRemoteRevision() {
+
+ @Override
+ public Root readRoot(ContentSession session) {
+ return root;
+ }
+
+ @Override
+ public String asString() {
+ return revisionId;
+ }
+
+ };
+ }
+
+ };
+ }
+
+}
\ No newline at end of file
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..42683ef
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRevision.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.ContentSession;
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.remote.RemoteRevision;
+
+interface ContentRemoteRevision extends RemoteRevision {
+
+ Root readRoot(ContentSession session);
+
+}
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..7a7b612
--- /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.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.Objects;
+import java.util.Set;
+import java.util.UUID;
+
+public 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.equals(revisionId, other.revisionId)) {
+ return false;
+ }
+
+ if (!Objects.equals(user, other.user)) {
+ return false;
+ }
+
+ if (!Objects.equals(principals, other.principals)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(revisionId, user, principals);
+ }
+
+ }
+
+ private Map roots;
+
+ public ContentRemoteRevisions() {
+ this.roots = Maps.newHashMap();
+ }
+
+ public 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..22064dd
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteSession.java
@@ -0,0 +1,358 @@
+/*
+ * 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.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.RemoteOperation;
+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 java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+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;
+
+ public ContentRemoteSession(ContentSession contentSession) {
+ this.contentSession = contentSession;
+ }
+
+ @Override
+ public RemoteRevision readLastRevision() {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public RemoteRevision readRevision(String revision) {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public RemoteTree 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.readRoot(contentSession);
+
+ if (root == null) {
+ return null;
+ }
+
+ Tree tree = root.getTree(path);
+
+ if (tree.exists()) {
+ return new ContentRemoteTree(tree, 0, filters);
+ }
+
+ return null;
+ }
+
+ @Override
+ public RemoteOperation 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 RemoteOperation 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 RemoteOperation 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(path, name, value);
+ }
+
+ @Override
+ public RemoteOperation 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 RemoteOperation 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 RemoteOperation 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 RemoteOperation createAggregateOperation(final List operations) {
+ if (operations == null) {
+ throw new IllegalArgumentException("operations not provided");
+ }
+
+ return new AggregateContentRemoteOperation(operations);
+ }
+
+ @Override
+ public RemoteRevision commit(RemoteRevision revision, RemoteOperation operation) {
+ 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.readRoot(contentSession);
+
+ try {
+ contentRemoteOperation.apply(root);
+ } catch (Exception e) {
+ return null;
+ }
+
+ try {
+ root.commit();
+ } catch (CommitFailedException e) {
+ return null;
+ }
+
+ return null;
+ }
+
+ @Override
+ public RemoteBinaryId readBinaryId(String binaryId) {
+ if (binaryId == null) {
+ throw new IllegalArgumentException("binary id not provided");
+ }
+
+ if (binaryId.isEmpty()) {
+ throw new IllegalArgumentException("invalid binary id");
+ }
+
+ Blob blob = contentSession.getLatestRoot().getBlob(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 RemoteBinaryId writeBinary(InputStream stream) {
+ if (stream == null) {
+ throw new IllegalArgumentException("stream not provided");
+ }
+
+ Blob blob;
+
+ try {
+ blob = contentSession.getLatestRoot().createBlob(stream);
+ } catch (IOException e) {
+ blob = null;
+ }
+
+ if (blob == null) {
+ return null;
+ }
+
+ return new ContentRemoteBinaryId(blob.getReference(), blob);
+ }
+
+}
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..3c13415
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteTree.java
@@ -0,0 +1,332 @@
+/*
+ * 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;
+
+ public ContentRemoteTree(Tree tree, int depth, RemoteTreeFilters filters) {
+ this.tree = tree;
+ this.depth = depth;
+ this.filters = filters;
+ }
+
+ @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 blob.getReference();
+ }
+
+ 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));
+ } 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..c8c7683
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/CopyContentRemoteOperation.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.content;
+
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.api.Tree;
+
+class CopyContentRemoteOperation implements ContentRemoteOperation {
+
+ 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) {
+ Tree sourceTree = root.getTree(source);
+
+ if (!sourceTree.exists()) {
+ throw new IllegalStateException("source tree does not exist");
+ }
+
+ Tree targetTree = root.getTree(target);
+
+ if (targetTree.exists()) {
+ throw new IllegalStateException("target tree already exists");
+ }
+
+ Tree targetParentTree = targetTree.getParent();
+
+ if (!targetParentTree.exists()) {
+ throw new IllegalStateException("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..fac5de2
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/MoveContentRemoteOperation.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+class MoveContentRemoteOperation implements ContentRemoteOperation {
+
+ 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) {
+ boolean success = root.move(source, target);
+
+ if (success) {
+ return;
+ }
+
+ throw new IllegalArgumentException("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..b6d1b28
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/RemoveContentRemoteOperation.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+class RemoveContentRemoteOperation implements ContentRemoteOperation {
+
+ private final String path;
+
+ public RemoveContentRemoteOperation(String path) {
+ this.path = path;
+ }
+
+ @Override
+ public void apply(Root root) {
+ Tree tree = root.getTree(path);
+
+ if (!tree.exists()) {
+ throw new IllegalStateException("tree does not exists");
+ }
+
+ if (!tree.remove()) {
+ throw new IllegalStateException("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..06a92ce
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetContentRemoteOperation.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.content;
+
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.remote.RemoteValue;
+
+class SetContentRemoteOperation implements ContentRemoteOperation {
+
+ private final String path;
+
+ private final String name;
+
+ private final RemoteValue value;
+
+ public SetContentRemoteOperation(String path, String name, RemoteValue value) {
+ this.path = path;
+ this.name = name;
+ this.value = value;
+ }
+
+ @Override
+ public void apply(Root root) {
+ Tree tree = root.getTree(path);
+
+ if (!tree.exists()) {
+ throw new IllegalStateException("tree does not exist");
+ }
+
+ value.whenType(new SetPropertyHandler(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..fda588e
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetPropertyHandler.java
@@ -0,0 +1,239 @@
+/*
+ * 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 Root root;
+
+ private final Tree tree;
+
+ private final String name;
+
+ public SetPropertyHandler(Root root, Tree tree, String name) {
+ 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(root, value), Type.BINARY);
+ }
+
+ @Override
+ public void isMultiBinaryId(Iterable value) {
+ tree.setProperty(name, getBlobsFromIds(root, 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(Root root, String binaryId) {
+ return root.getBlob(binaryId);
+ }
+
+ private Iterable getBlobsFromIds(final Root root, Iterable binaryIds) {
+ return Iterables.transform(binaryIds, new Function() {
+
+ @Override
+ public Blob apply(String binaryId) {
+ return getBlobFromId(root, 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..20ef5f0
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/UnsetContentRemoteOperation.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.content;
+
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.api.Tree;
+
+class UnsetContentRemoteOperation implements ContentRemoteOperation {
+
+ 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) {
+ Tree tree = root.getTree(path);
+
+ if (!tree.exists()) {
+ throw new IllegalStateException("tree does not exists");
+ }
+
+ if (!tree.hasProperty(name)) {
+ throw new IllegalStateException("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..63071a7
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteHandler.java
@@ -0,0 +1,47 @@
+/*
+ * 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.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+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 Exception {
+ 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/RemoteServer.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteServer.java
new file mode 100644
index 0000000..901ca07
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteServer.java
@@ -0,0 +1,64 @@
+/*
+ * 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.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.servlet.ServletHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+
+import java.net.InetSocketAddress;
+
+public class RemoteServer {
+
+ private final Server server;
+
+ public RemoteServer(RemoteRepository repository, String host, int port) {
+ this.server = createServer(repository, new InetSocketAddress(host, port));
+ }
+
+ private Server createServer(RemoteRepository repository, InetSocketAddress address) {
+ Server server = new Server(address);
+
+ server.setHandler(createHandler(repository));
+
+ return server;
+ }
+
+ private Handler createHandler(RemoteRepository repository) {
+ ServletHandler handler = new ServletHandler();
+
+ handler.addServletWithMapping(new ServletHolder(new RemoteServlet(repository)), "/*");
+
+ return handler;
+ }
+
+ public void start() throws Exception {
+ server.start();
+ }
+
+ public void stop() throws Exception {
+ server.stop();
+ }
+
+ public void join() throws Exception {
+ server.join();
+ }
+
+}
\ No newline at end of file
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..0ac9e71
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteServlet.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.http;
+
+import org.apache.jackrabbit.oak.remote.RemoteRepository;
+import org.apache.jackrabbit.oak.remote.http.handler.Handler;
+
+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.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.createNotFoundHandler;
+import static org.apache.jackrabbit.oak.remote.http.matcher.Matchers.matchesRequest;
+
+public class RemoteServlet extends HttpServlet {
+
+ 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) {
+ throw e;
+ } catch (IOException e) {
+ throw e;
+ } catch (Exception 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("get", "/revisions/[^/]+/tree/.*", createGetRevisionTreeHandler())
+ );
+ }
+
+ 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..cb829e0
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/AuthenticationWrapperHandler.java
@@ -0,0 +1,161 @@
+/*
+ * 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.RemoteRepository;
+import org.apache.jackrabbit.oak.remote.RemoteSession;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Base64;
+
+class AuthenticationWrapperHandler implements Handler {
+
+ 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 Exception {
+ RemoteSession session = (RemoteSession) request.getAttribute("session");
+
+ if (session == null) {
+ session = login(request);
+ }
+
+ if (session != null) {
+ request.setAttribute("session", session);
+ }
+
+ if (session != null) {
+ authenticated.handle(request, response);
+ } else {
+ notAuthenticated.handle(request, response);
+ }
+ }
+
+ private RemoteSession login(HttpServletRequest request) {
+ RemoteRepository repository = (RemoteRepository) request.getAttribute("repository");
+
+ if (repository == null) {
+ return null;
+ }
+
+ 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 = new String(Base64.getDecoder().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.login(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/ForbiddenHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/ForbiddenHandler.java
new file mode 100644
index 0000000..99ce358
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/ForbiddenHandler.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.http.handler;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+class ForbiddenHandler implements Handler {
+
+ @Override
+ public void handle(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ }
+
+}
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..671ce80
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastRevisionHandler.java
@@ -0,0 +1,57 @@
+/*
+ * 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;
+
+class GetLastRevisionHandler implements Handler {
+
+ @Override
+ public void handle(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ RemoteSession session = (RemoteSession) request.getAttribute("session");
+
+ if (session == null) {
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ 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..e80b282
--- /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 mapped to 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..2474398
--- /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 mapped to 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 mapped to 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..cda60ab
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetTreeHandler.java
@@ -0,0 +1,465 @@
+/*
+ * 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 com.google.common.io.BaseEncoding;
+import com.google.common.io.ByteStreams;
+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.io.InputStream;
+import java.math.BigDecimal;
+import java.util.Map;
+import java.util.Set;
+
+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 Exception {
+ RemoteSession session = (RemoteSession) request.getAttribute("session");
+
+ if (session == null) {
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ return;
+ }
+
+ RemoteRevision revision = readRevision(request, session);
+
+ if (revision == null) {
+ response.setStatus(HttpServletResponse.SC_GONE);
+ return;
+ }
+
+ RemoteTree tree = session.readTree(revision, readPath(request), readFilters(request));
+
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.setContentType("application/json");
+
+ ServletOutputStream stream = response.getOutputStream();
+
+ JsonGenerator generator = new JsonFactory().createJsonGenerator(stream, JsonEncoding.UTF8);
+ renderResponse(generator, revision, tree);
+ generator.flush();
+
+ stream.close();
+ }
+
+ private void renderResponse(JsonGenerator generator, RemoteRevision revision, RemoteTree tree) throws IOException {
+ generator.writeStartObject();
+ generator.writeFieldName("revision");
+ generator.writeString(revision.asString());
+ generator.writeFieldName("tree");
+ renderTree(generator, tree);
+ generator.writeEndObject();
+ }
+
+ private void renderTree(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());
+ renderTree(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());
+ renderProperty(generator, entry.getValue());
+ }
+
+ generator.writeEndObject();
+ }
+
+ private void renderProperty(final 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 interface GeneratorWriter {
+
+ void write(JsonGenerator generator, T value) throws IOException;
+
+ }
+
+ private 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 GeneratorWriter getStringWriter() {
+ return new GeneratorWriter() {
+
+ @Override
+ public void write(JsonGenerator generator, String value) throws IOException {
+ generator.writeString(value);
+ }
+
+ };
+ }
+
+ private GeneratorWriter getBooleanWriter() {
+ return new GeneratorWriter() {
+
+ @Override
+ public void write(JsonGenerator generator, Boolean value) throws IOException {
+ generator.writeBoolean(value);
+ }
+
+ };
+ }
+
+ private GeneratorWriter getLongWriter() {
+ return new GeneratorWriter() {
+
+ @Override
+ public void write(JsonGenerator generator, Long value) throws IOException {
+ generator.writeNumber(value);
+ }
+
+ };
+ }
+
+ private GeneratorWriter getDecimalWriter() {
+ return new GeneratorWriter() {
+
+ @Override
+ public void write(JsonGenerator generator, BigDecimal value) throws IOException {
+ generator.writeString(value.toString());
+ }
+
+ };
+ }
+
+ private GeneratorWriter getDoubleWriter() {
+ return new GeneratorWriter() {
+
+ @Override
+ public void write(JsonGenerator generator, Double value) throws IOException {
+ generator.writeNumber(value);
+ }
+
+ };
+ }
+
+ private 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 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 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..4eec8d7
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/Handler.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+public interface Handler {
+
+ void handle(HttpServletRequest request, HttpServletResponse response) throws Exception;
+
+}
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..481b532
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/Handlers.java
@@ -0,0 +1,76 @@
+/*
+ * 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();
+ }
+
+ private static Handler withAuthentication(Handler authenticated) {
+ return new AuthenticationWrapperHandler(authenticated, createForbiddenHandler());
+ }
+
+ private static Handler createForbiddenHandler() {
+ return new ForbiddenHandler();
+ }
+
+}
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..538d65e
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/NotFoundHandler.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.http.handler;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+class NotFoundHandler implements Handler {
+
+ @Override
+ public void handle(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ }
+
+}
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..c30fd6c
--- /dev/null
+++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matchers.java
@@ -0,0 +1,102 @@
+/*
+ * 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) {
+ 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/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..e0f8053
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/AddContentRemoteOperationTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.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() {
+ 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 = IllegalStateException.class)
+ public void testAddNodeWithExistingTree() {
+ 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 = IllegalStateException.class)
+ public void testAddNodeWithNonExistingParent() {
+ 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..9f2c8e3
--- /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.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.junit.Assert.assertNull;
+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() {
+ createRepository().login(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testLoginWithInvalidCredentials() {
+ 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
+ public void testUnsuccessfulLoginWithBasicCredentials() throws Exception {
+ ContentRepository repository = mock(ContentRepository.class);
+ when(repository.login(any(Credentials.class), anyString())).thenThrow(LoginException.class);
+
+ ContentRemoteRepository remoteRepository = createRepository(repository);
+ assertNull(remoteRepository.login(remoteRepository.createBasicCredentials("admin", "admin".toCharArray())));
+ }
+
+}
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..cd337b5
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteSessionTest.java
@@ -0,0 +1,480 @@
+/*
+ * 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.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.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.Ignore;
+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.any;
+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(ContentSession session) {
+ return new ContentRemoteSession(session);
+ }
+
+ @Test
+ @Ignore
+ public void testReadLastRevision() {
+ assertNotNull(createSession().readLastRevision());
+ }
+
+ @Test
+ @Ignore
+ public void testReadLastRevisionAsString() {
+ assertNotNull(createSession().readLastRevision().asString());
+ }
+
+ @Test
+ @Ignore
+ public void testReadRevision() {
+ ContentRemoteSession session = createSession();
+ assertNotNull(session.readRevision(session.readLastRevision().asString()));
+ }
+
+ @Test
+ @Ignore
+ public void testReadRevisionAsString() {
+ ContentRemoteSession session = createSession();
+ String revision = session.readLastRevision().asString();
+ assertEquals(revision, session.readRevision(revision).asString());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testReadTreeWithNullRevision() {
+ createSession().readTree(null, "/", new RemoteTreeFilters());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testReadTreeWithInvalidRevision() {
+ createSession().readTree(mock(RemoteRevision.class), "/", new RemoteTreeFilters());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testReadTreeWithNullPath() {
+ ContentRemoteRevision revision = mock(ContentRemoteRevision.class);
+ createSession().readTree(revision, null, new RemoteTreeFilters());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testReadTreeWithInvalidPath() {
+ ContentRemoteRevision revision = mock(ContentRemoteRevision.class);
+ createSession().readTree(revision, "invalid", new RemoteTreeFilters());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testReadTreeWithNullFilters() {
+ ContentRemoteRevision revision = mock(ContentRemoteRevision.class);
+ createSession().readTree(revision, "/", null);
+ }
+
+ @Test
+ public void testReadNonExistingTree() {
+ 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).readRoot(any(ContentSession.class));
+
+ assertNull(createSession().readTree(revision, "/", new RemoteTreeFilters()));
+ }
+
+ @Test
+ public void testReadExistingTree() {
+ 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).readRoot(any(ContentSession.class));
+
+ 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
+ @Ignore
+ public void testCommit() {
+ Root root = mock(Root.class);
+
+ ContentRemoteOperation operation = mock(ContentRemoteOperation.class);
+
+ ContentRemoteRevision revision = mock(ContentRemoteRevision.class);
+ doReturn(root).when(revision).readRoot(any(ContentSession.class));
+
+ assertNotNull(createSession().commit(revision, operation));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCommitWithNullRevision() {
+ createSession().commit(null, mock(ContentRemoteOperation.class));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCommitWithInvalidRevision() {
+ createSession().commit(mock(RemoteRevision.class), mock(ContentRemoteOperation.class));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCommitWithNullOperation() {
+ createSession().commit(mock(ContentRemoteRevision.class), null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCommitWithInvalidOperation() {
+ createSession().commit(mock(ContentRemoteRevision.class), mock(RemoteOperation.class));
+ }
+
+ @Test
+ public void testCommitWithOperationThrowingException() {
+ ContentRemoteOperation operation = mock(ContentRemoteOperation.class);
+ doThrow(IllegalStateException.class).when(operation).apply(any(Root.class));
+
+ assertNull(createSession().commit(mock(ContentRemoteRevision.class), operation));
+ }
+
+ @Test
+ 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).readRoot(any(ContentSession.class));
+
+ assertNull(createSession().commit(revision, mock(ContentRemoteOperation.class)));
+ }
+
+ @Test
+ public void testReadBinaryId() {
+ Blob blob = mock(Blob.class);
+
+ Root root = mock(Root.class);
+ doReturn(blob).when(root).getBlob(anyString());
+
+ ContentSession session = mock(ContentSession.class);
+ doReturn(root).when(session).getLatestRoot();
+
+ ContentRemoteSession remoteSession = createSession(session);
+ assertNotNull(remoteSession.readBinaryId("id"));
+ }
+
+ @Test
+ public void testReadBinaryIdAsString() {
+ Blob blob = mock(Blob.class);
+
+ Root root = mock(Root.class);
+ doReturn(blob).when(root).getBlob(anyString());
+
+ ContentSession session = mock(ContentSession.class);
+ doReturn(root).when(session).getLatestRoot();
+
+ ContentRemoteSession remoteSession = createSession(session);
+ 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);
+
+ Root root = mock(Root.class);
+ doReturn(blob).when(root).createBlob(any(InputStream.class));
+
+ ContentSession session = mock(ContentSession.class);
+ doReturn(root).when(session).getLatestRoot();
+
+ ContentRemoteSession remoteSession = createSession(session);
+ assertNotNull(remoteSession.writeBinary(mock(InputStream.class)));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testWriteBinaryWithNullStream() {
+ createSession().writeBinary(null);
+ }
+
+ @Test
+ public void testWriteBlobFailure() throws Exception {
+ Root root = mock(Root.class);
+ doThrow(IOException.class).when(root).createBlob(any(InputStream.class));
+
+ ContentSession session = mock(ContentSession.class);
+ doReturn(root).when(session).getLatestRoot();
+
+ ContentRemoteSession remoteSession = createSession(session);
+ assertNull(remoteSession.writeBinary(mock(InputStream.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..a2c6ff9
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteTreeTest.java
@@ -0,0 +1,939 @@
+/*
+ * 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());
+ }
+
+ private ContentRemoteTree createTree(Tree tree, RemoteTreeFilters filters) {
+ return new ContentRemoteTree(tree, 0, filters);
+ }
+
+ @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);
+ doReturn("id").when(blob).getReference();
+
+ 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();
+
+ Map properties = createTree(tree).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);
+ doReturn("id").when(blob).getReference();
+
+ 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();
+
+ Map properties = createTree(tree).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 properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isPath());
+ assertEquals("value", properties.get("name").asPath());
+ }
+
+ @Test
+ public void testGetMultiPathProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.PATHS).when(property).getType();
+ doReturn(singletonList("value")).when(property).getValue(Type.PATHS);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isMultiPath());
+ assertEquals("value", getOnlyElement(properties.get("name").asMultiPath()));
+ }
+
+ @Test
+ public void testReferenceProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.REFERENCE).when(property).getType();
+ doReturn("value").when(property).getValue(Type.REFERENCE);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isReference());
+ assertEquals("value", properties.get("name").asReference());
+ }
+
+ @Test
+ public void testGetMultiReferenceProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.REFERENCES).when(property).getType();
+ doReturn(singletonList("value")).when(property).getValue(Type.REFERENCES);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isMultiReference());
+ assertEquals("value", getOnlyElement(properties.get("name").asMultiReference()));
+ }
+
+ @Test
+ public void testTextProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.STRING).when(property).getType();
+ doReturn("value").when(property).getValue(Type.STRING);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isText());
+ assertEquals("value", properties.get("name").asText());
+ }
+
+ @Test
+ public void testGetMultiTextProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.STRINGS).when(property).getType();
+ doReturn(singletonList("value")).when(property).getValue(Type.STRINGS);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isMultiText());
+ assertEquals("value", getOnlyElement(properties.get("name").asMultiText()));
+ }
+
+ @Test
+ public void testUriProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.URI).when(property).getType();
+ doReturn("value").when(property).getValue(Type.URI);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isUri());
+ assertEquals("value", properties.get("name").asUri());
+ }
+
+ @Test
+ public void testGetMultiUriProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.URIS).when(property).getType();
+ doReturn(singletonList("value")).when(property).getValue(Type.URIS);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isMultiUri());
+ assertEquals("value", getOnlyElement(properties.get("name").asMultiUri()));
+ }
+
+ @Test
+ public void testWeakReferenceProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.WEAKREFERENCE).when(property).getType();
+ doReturn("value").when(property).getValue(Type.WEAKREFERENCE);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isWeakReference());
+ assertEquals("value", properties.get("name").asWeakReference());
+ }
+
+ @Test
+ public void testGetMultiWeakReferenceProperty() {
+ PropertyState property = mock(PropertyState.class);
+ doReturn("name").when(property).getName();
+ doReturn(Type.WEAKREFERENCES).when(property).getType();
+ doReturn(singletonList("value")).when(property).getValue(Type.WEAKREFERENCES);
+
+ Tree tree = mock(Tree.class);
+ doReturn(singletonList(property)).when(tree).getProperties();
+
+ Map properties = createTree(tree).getProperties();
+
+ assertTrue(properties.containsKey("name"));
+ assertTrue(properties.get("name").isMultiWeakReference());
+ assertEquals("value", getOnlyElement(properties.get("name").asMultiWeakReference()));
+ }
+
+ @Test
+ public void testFilterPropertyIn() {
+ PropertyState fooProperty = mock(PropertyState.class);
+ doReturn("foo").when(fooProperty).getName();
+ doReturn(Type.BOOLEAN).when(fooProperty).getType();
+ doReturn(true).when(fooProperty).getValue(Type.BOOLEAN);
+
+ PropertyState barProperty = mock(PropertyState.class);
+ doReturn("bar").when(barProperty).getName();
+ doReturn(Type.BOOLEAN).when(barProperty).getType();
+ doReturn(true).when(barProperty).getValue(Type.BOOLEAN);
+
+ Tree tree = mock(Tree.class);
+ doReturn(asList(fooProperty, barProperty)).when(tree).getProperties();
+
+ ContentRemoteTree remoteTree = createTree(tree, new RemoteTreeFilters() {
+
+ @Override
+ public Set getPropertyFilters() {
+ return newHashSet("foo");
+ }
+
+ });
+
+ Map properties = remoteTree.getProperties();
+
+ assertTrue(properties.containsKey("foo"));
+ assertFalse(properties.containsKey("bar"));
+ }
+
+ @Test
+ public void testFilterPropertyOut() {
+ PropertyState fooProperty = mock(PropertyState.class);
+ doReturn("foo").when(fooProperty).getName();
+ doReturn(Type.BOOLEAN).when(fooProperty).getType();
+ doReturn(true).when(fooProperty).getValue(Type.BOOLEAN);
+
+ PropertyState barProperty = mock(PropertyState.class);
+ doReturn("bar").when(barProperty).getName();
+ doReturn(Type.BOOLEAN).when(barProperty).getType();
+ doReturn(true).when(barProperty).getValue(Type.BOOLEAN);
+
+ Tree tree = mock(Tree.class);
+ doReturn(asList(fooProperty, barProperty)).when(tree).getProperties();
+
+ ContentRemoteTree remoteTree = createTree(tree, new RemoteTreeFilters() {
+
+ @Override
+ public Set getPropertyFilters() {
+ return newHashSet("-bar");
+ }
+
+ });
+
+ Map properties = remoteTree.getProperties();
+
+ assertTrue(properties.containsKey("foo"));
+ assertFalse(properties.containsKey("bar"));
+ }
+
+ @Test
+ public void testGetChildrenMaxDepth() {
+ Tree child = mock(Tree.class);
+ doReturn("child").when(child).getName();
+
+ Tree tree = mock(Tree.class);
+ doReturn(asList(child)).when(tree).getChildren();
+
+ Map children = createTree(tree).getChildren();
+
+ assertTrue(children.containsKey("child"));
+ assertNull(children.get("child"));
+ }
+
+ @Test
+ public void testGetChildren() {
+ Tree child = mock(Tree.class);
+ doReturn("child").when(child).getName();
+
+ Tree tree = mock(Tree.class);
+ doReturn(asList(child)).when(tree).getChildren();
+
+ ContentRemoteTree remoteTree = createTree(tree, new RemoteTreeFilters() {
+
+ @Override
+ public int getDepth() {
+ return 1;
+ }
+
+ });
+
+ Map children = remoteTree.getChildren();
+
+ assertTrue(children.containsKey("child"));
+ assertNotNull(children.get("child"));
+ }
+
+ @Test
+ public void testGetChildrenWithStart() {
+ Tree foo = mock(Tree.class);
+ doReturn("foo").when(foo).getName();
+
+ Tree bar = mock(Tree.class);
+ doReturn("bar").when(bar).getName();
+
+ Tree tree = mock(Tree.class);
+ doReturn(asList(foo, bar)).when(tree).getChildren();
+
+ ContentRemoteTree remoteTree = createTree(tree, new RemoteTreeFilters() {
+
+ @Override
+ public int getChildrenStart() {
+ return 1;
+ }
+
+ });
+
+ Map children = remoteTree.getChildren();
+
+ assertFalse(children.containsKey("foo"));
+ assertTrue(children.containsKey("bar"));
+ }
+
+ @Test
+ public void testGetChildrenWithNegativeStart() {
+ Tree foo = mock(Tree.class);
+ doReturn("foo").when(foo).getName();
+
+ Tree bar = mock(Tree.class);
+ doReturn("bar").when(bar).getName();
+
+ Tree tree = mock(Tree.class);
+ doReturn(asList(foo, bar)).when(tree).getChildren();
+
+ ContentRemoteTree remoteTree = createTree(tree, new RemoteTreeFilters() {
+
+ @Override
+ public int getChildrenStart() {
+ return -1;
+ }
+
+ });
+
+ Map children = remoteTree.getChildren();
+
+ assertTrue(children.containsKey("foo"));
+ assertTrue(children.containsKey("bar"));
+ }
+
+ @Test
+ public void testGetChildrenWithStartTooBig() {
+ Tree foo = mock(Tree.class);
+ doReturn("foo").when(foo).getName();
+
+ Tree bar = mock(Tree.class);
+ doReturn("bar").when(bar).getName();
+
+ Tree tree = mock(Tree.class);
+ doReturn(asList(foo, bar)).when(tree).getChildren();
+
+ ContentRemoteTree remoteTree = createTree(tree, new RemoteTreeFilters() {
+
+ @Override
+ public int getChildrenStart() {
+ return 2;
+ }
+
+ });
+
+ Map children = remoteTree.getChildren();
+
+ assertFalse(children.containsKey("foo"));
+ assertFalse(children.containsKey("bar"));
+ }
+
+ @Test
+ public void testGetChildrenWithCount() {
+ Tree foo = mock(Tree.class);
+ doReturn("foo").when(foo).getName();
+
+ Tree bar = mock(Tree.class);
+ doReturn("bar").when(bar).getName();
+
+ Tree tree = mock(Tree.class);
+ doReturn(asList(foo, bar)).when(tree).getChildren();
+
+ ContentRemoteTree remoteTree = createTree(tree, new RemoteTreeFilters() {
+
+ @Override
+ public int getChildrenCount() {
+ return 1;
+ }
+ });
+
+ Map children = remoteTree.getChildren();
+
+ assertTrue(children.containsKey("foo"));
+ assertFalse(children.containsKey("bar"));
+ }
+
+ @Test
+ public void testGetChildrenWithNegativeCount() {
+ Tree foo = mock(Tree.class);
+ doReturn("foo").when(foo).getName();
+
+ Tree bar = mock(Tree.class);
+ doReturn("bar").when(bar).getName();
+
+ Tree tree = mock(Tree.class);
+ doReturn(asList(foo, bar)).when(tree).getChildren();
+
+ ContentRemoteTree remoteTree = createTree(tree, new RemoteTreeFilters() {
+
+ @Override
+ public int getChildrenCount() {
+ return -1;
+ }
+ });
+
+ Map children = remoteTree.getChildren();
+
+ assertTrue(children.containsKey("foo"));
+ assertTrue(children.containsKey("bar"));
+ }
+
+ @Test
+ public void testGetChildrenWithZeroCount() {
+ Tree foo = mock(Tree.class);
+ doReturn("foo").when(foo).getName();
+
+ Tree bar = mock(Tree.class);
+ doReturn("bar").when(bar).getName();
+
+ Tree tree = mock(Tree.class);
+ doReturn(asList(foo, bar)).when(tree).getChildren();
+
+ ContentRemoteTree remoteTree = createTree(tree, new RemoteTreeFilters() {
+
+ @Override
+ public int getChildrenCount() {
+ return 0;
+ }
+ });
+
+ Map children = remoteTree.getChildren();
+
+ assertFalse(children.containsKey("foo"));
+ assertFalse(children.containsKey("bar"));
+ }
+
+ @Test
+ public void testGetChildrenWithCountTooBig() {
+ Tree foo = mock(Tree.class);
+ doReturn("foo").when(foo).getName();
+
+ Tree bar = mock(Tree.class);
+ doReturn("bar").when(bar).getName();
+
+ Tree tree = mock(Tree.class);
+ doReturn(asList(foo, bar)).when(tree).getChildren();
+
+ ContentRemoteTree remoteTree = createTree(tree, new RemoteTreeFilters() {
+
+ @Override
+ public int getChildrenCount() {
+ return 3;
+ }
+ });
+
+ Map children = remoteTree.getChildren();
+
+ assertTrue(children.containsKey("foo"));
+ assertTrue(children.containsKey("bar"));
+ }
+
+ @Test
+ public void testGetChildrenWithSlicing() {
+ Tree foo = mock(Tree.class);
+ doReturn("foo").when(foo).getName();
+
+ Tree bar = mock(Tree.class);
+ doReturn("bar").when(bar).getName();
+
+ Tree baz = mock(Tree.class);
+ doReturn("baz").when(baz).getName();
+
+ Tree tree = mock(Tree.class);
+ doReturn(asList(foo, bar, baz)).when(tree).getChildren();
+
+ ContentRemoteTree remoteTree = createTree(tree, new RemoteTreeFilters() {
+
+ @Override
+ public int getChildrenStart() {
+ return 1;
+ }
+
+ @Override
+ public int getChildrenCount() {
+ return 1;
+ }
+ });
+
+ Map children = remoteTree.getChildren();
+
+ assertFalse(children.containsKey("foo"));
+ assertTrue(children.containsKey("bar"));
+ assertFalse(children.containsKey("baz"));
+ }
+
+ @Test
+ public void testGetChildrenWithIncludeFilters() {
+ Tree foo = mock(Tree.class);
+ doReturn("foo").when(foo).getName();
+
+ Tree bar = mock(Tree.class);
+ doReturn("bar").when(bar).getName();
+
+ Tree tree = mock(Tree.class);
+ doReturn(asList(foo, bar)).when(tree).getChildren();
+
+ ContentRemoteTree remoteTree = createTree(tree, new RemoteTreeFilters() {
+
+ @Override
+ public Set getNodeFilters() {
+ return newHashSet("foo");
+ }
+
+ });
+
+ Map children = remoteTree.getChildren();
+
+ assertTrue(children.containsKey("foo"));
+ assertFalse(children.containsKey("bar"));
+ }
+
+ @Test
+ public void testGetChildrenWithExcludeFilters() {
+ Tree foo = mock(Tree.class);
+ doReturn("foo").when(foo).getName();
+
+ Tree bar = mock(Tree.class);
+ doReturn("bar").when(bar).getName();
+
+ Tree tree = mock(Tree.class);
+ doReturn(asList(foo, bar)).when(tree).getChildren();
+
+ ContentRemoteTree remoteTree = createTree(tree, new RemoteTreeFilters() {
+
+ @Override
+ public Set getNodeFilters() {
+ return newHashSet("-bar");
+ }
+
+ });
+
+ Map children = remoteTree.getChildren();
+
+ assertTrue(children.containsKey("foo"));
+ assertFalse(children.containsKey("bar"));
+ }
+
+ @Test
+ public void testGetChildrenWithSlicingAndFiltering() {
+ Tree bar = mock(Tree.class);
+ doReturn("bar").when(bar).getName();
+
+ Tree foo = mock(Tree.class);
+ doReturn("foo").when(foo).getName();
+
+ Tree baz = mock(Tree.class);
+ doReturn("baz").when(baz).getName();
+
+ Tree tree = mock(Tree.class);
+ doReturn(asList(bar, foo, baz)).when(tree).getChildren();
+
+ ContentRemoteTree remoteTree = createTree(tree, new RemoteTreeFilters() {
+
+ @Override
+ public Set getNodeFilters() {
+ return Sets.newHashSet("ba*");
+ }
+
+ @Override
+ public int getChildrenStart() {
+ return 1;
+ }
+
+ @Override
+ public int getChildrenCount() {
+ return 1;
+ }
+ });
+
+ Map children = remoteTree.getChildren();
+
+ assertFalse(children.containsKey("bar"));
+ assertFalse(children.containsKey("foo"));
+ assertFalse(children.containsKey("baz"));
+ }
+
+}
diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/CopyContentRemoteOperationTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/CopyContentRemoteOperationTest.java
new file mode 100644
index 0000000..597653d
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/CopyContentRemoteOperationTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.junit.Test;
+
+import static java.util.Collections.emptyList;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class CopyContentRemoteOperationTest {
+
+ CopyContentRemoteOperation createOperation(String source, String target) {
+ return new CopyContentRemoteOperation(source, target);
+ }
+
+ @Test
+ public void testCopy() {
+ Tree parent = mock(Tree.class);
+ doReturn(true).when(parent).exists();
+
+ Tree target = mock(Tree.class);
+ doReturn(false).when(target).exists();
+ doReturn(parent).when(target).getParent();
+ doReturn("target").when(target).getName();
+
+ Tree source = mock(Tree.class);
+ doReturn(true).when(source).exists();
+ doReturn(emptyList()).when(source).getProperties();
+ doReturn(emptyList()).when(source).getChildren();
+
+ Root root = mock(Root.class);
+ doReturn(source).when(root).getTree("/source");
+ doReturn(target).when(root).getTree("/target");
+
+ createOperation("/source", "/target").apply(root);
+
+ verify(parent).addChild("target");
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testCopyWithNonExistingSource() {
+ Tree source = mock(Tree.class);
+ doReturn(false).when(source).exists();
+
+ Root root = mock(Root.class);
+ doReturn(source).when(root).getTree("/source");
+
+ createOperation("/source", "/target").apply(root);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testCopyWithExistingTarget() {
+ Tree target = mock(Tree.class);
+ doReturn(true).when(target).exists();
+
+ Tree source = mock(Tree.class);
+ doReturn(true).when(source).exists();
+
+ Root root = mock(Root.class);
+ doReturn(source).when(root).getTree("/source");
+ doReturn(target).when(root).getTree("/target");
+
+ createOperation("/source", "/target").apply(root);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testCopyWithNonExistingTargetParent() {
+ Tree parent = mock(Tree.class);
+ doReturn(false).when(parent).exists();
+
+ Tree target = mock(Tree.class);
+ doReturn(false).when(target).exists();
+ doReturn(parent).when(target).getParent();
+
+ Tree source = mock(Tree.class);
+ doReturn(true).when(source).exists();
+
+ Root root = mock(Root.class);
+ doReturn(source).when(root).getTree("/source");
+ doReturn(target).when(root).getTree("/target");
+
+ createOperation("/source", "/target").apply(root);
+ }
+
+}
diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/MoveContentRemoteOperationTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/MoveContentRemoteOperationTest.java
new file mode 100644
index 0000000..630083e
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/MoveContentRemoteOperationTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.junit.Test;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+public class MoveContentRemoteOperationTest {
+
+ MoveContentRemoteOperation createOperation(String source, String target) {
+ return new MoveContentRemoteOperation(source, target);
+ }
+
+ @Test
+ public void testMove() {
+ Root root = mock(Root.class);
+ doReturn(true).when(root).move("/source", "/target");
+
+ createOperation("/source", "/target").apply(root);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testMoveUnsuccessful() {
+ Root root = mock(Root.class);
+ doReturn(false).when(root).move("/source", "/target");
+
+ createOperation("/source", "/target").apply(root);
+ }
+
+}
diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/RemoveContentRemoteOperationTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/RemoveContentRemoteOperationTest.java
new file mode 100644
index 0000000..29b0dbb
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/RemoveContentRemoteOperationTest.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.content;
+
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.junit.Test;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+public class RemoveContentRemoteOperationTest {
+
+ private RemoveContentRemoteOperation createOperation(String path) {
+ return new RemoveContentRemoteOperation(path);
+ }
+
+ @Test
+ public void testRemove() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+ doReturn(true).when(tree).remove();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test").apply(root);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testRemoveWithNonExistingTree() {
+ Tree tree = mock(Tree.class);
+ doReturn(false).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test").apply(root);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testRemoveWithNonRemovableTree() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+ doReturn(false).when(tree).remove();
+
+ 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/SetContentRemoteOperationTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/SetContentRemoteOperationTest.java
new file mode 100644
index 0000000..86d21b6
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/SetContentRemoteOperationTest.java
@@ -0,0 +1,526 @@
+/*
+ * 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.Predicate;
+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;
+import org.apache.jackrabbit.oak.remote.RemoteValue.Supplier;
+import org.apache.jackrabbit.util.ISO8601;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.junit.Test;
+
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.util.Calendar;
+
+import static com.google.common.collect.Iterables.any;
+import static java.util.Collections.singletonList;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toBinary;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toBinaryId;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toBoolean;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toDate;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toDecimal;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toDouble;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toLong;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toMultiBinary;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toMultiBinaryId;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toMultiBoolean;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toMultiDate;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toMultiDecimal;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toMultiDouble;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toMultiLong;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toMultiName;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toMultiPath;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toMultiReference;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toMultiText;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toMultiUri;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toMultiWeakReference;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toName;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toPath;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toReference;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toText;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toUri;
+import static org.apache.jackrabbit.oak.remote.RemoteValue.toWeakReference;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class SetContentRemoteOperationTest {
+
+ private SetContentRemoteOperation createOperation(String path, String name, RemoteValue value) {
+ return new SetContentRemoteOperation(path, name, value);
+ }
+
+ private Matcher> isIterableReferencing(final T value) {
+ return new BaseMatcher>() {
+
+ @Override
+ public boolean matches(Object item) {
+ Iterable iterable = null;
+
+ if (item instanceof Iterable) {
+ iterable = (Iterable) item;
+ }
+
+ if (iterable == null) {
+ return false;
+ }
+
+ return any(iterable, new Predicate() {
+
+ @Override
+ public boolean apply(Object element) {
+ return element == value;
+ }
+
+ });
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("an iterable referencing ").appendValue(value);
+ }
+
+ };
+ }
+
+ private Matcher> isIterableContaining(final T value) {
+ return new BaseMatcher>() {
+
+ @Override
+ public boolean matches(Object item) {
+ Iterable iterable = null;
+
+ if (item instanceof Iterable) {
+ iterable = (Iterable) item;
+ }
+
+ if (iterable == null) {
+ return false;
+ }
+
+ return any(iterable, new Predicate() {
+
+ @Override
+ public boolean apply(Object element) {
+ if (element == null && value == null) {
+ return true;
+ }
+
+ if (element != null && value != null) {
+ return element.equals(value);
+ }
+
+ return false;
+ }
+
+ });
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("an iterable containing ").appendValue(value);
+ }
+
+ };
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testSetWithNonExistingTree() {
+ Tree tree = mock(Tree.class);
+ doReturn(false).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", mock(RemoteValue.class)).apply(root);
+ }
+
+ @Test
+ public void testSetBinaryProperty() throws Exception {
+ Blob blob = mock(Blob.class);
+
+ InputStream stream = mock(InputStream.class);
+
+ Supplier supplier = mock(Supplier.class);
+ doReturn(stream).when(supplier).get();
+
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+ doReturn(blob).when(root).createBlob(stream);
+
+ createOperation("/test", "name", toBinary(supplier)).apply(root);
+
+ verify(tree).setProperty("name", blob, Type.BINARY);
+ }
+
+ @Test
+ public void testSetMultiBinaryProperty() throws Exception {
+ Blob blob = mock(Blob.class);
+
+ InputStream stream = mock(InputStream.class);
+
+ Supplier supplier = mock(Supplier.class);
+ doReturn(stream).when(supplier).get();
+
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+ doReturn(blob).when(root).createBlob(stream);
+
+ createOperation("/test", "name", toMultiBinary(singletonList(supplier))).apply(root);
+
+ verify(tree).setProperty(eq("name"), argThat(isIterableReferencing(blob)), eq(Type.BINARIES));
+ }
+
+ @Test
+ public void testSetBinaryIdProperty() {
+ Blob blob = mock(Blob.class);
+
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+ doReturn(blob).when(root).getBlob("id");
+
+ createOperation("/test", "name", toBinaryId("id")).apply(root);
+
+ verify(tree).setProperty("name", blob, Type.BINARY);
+ }
+
+ @Test
+ public void setMultiBinaryIdProperty() {
+ Blob blob = mock(Blob.class);
+
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+ doReturn(blob).when(root).getBlob("id");
+
+ createOperation("/test", "name", toMultiBinaryId(singletonList("id"))).apply(root);
+
+ verify(tree).setProperty(eq("name"), argThat(isIterableReferencing(blob)), eq(Type.BINARIES));
+ }
+
+ @Test
+ public void testSetBooleanProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toBoolean(true)).apply(root);
+
+ verify(tree).setProperty("name", true, Type.BOOLEAN);
+ }
+
+ @Test
+ public void testSetMultiBooleanProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toMultiBoolean(singletonList(true))).apply(root);
+
+ verify(tree).setProperty(eq("name"), argThat(isIterableContaining(true)), eq(Type.BOOLEANS));
+ }
+
+ @Test
+ public void testSetDateProperty() {
+ Calendar calendar = Calendar.getInstance();
+
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toDate(calendar.getTimeInMillis())).apply(root);
+
+ verify(tree).setProperty("name", ISO8601.format(calendar), Type.DATE);
+ }
+
+ @Test
+ public void testSetMultiDateProperty() {
+ Calendar calendar = Calendar.getInstance();
+
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toMultiDate(singletonList(calendar.getTimeInMillis()))).apply(root);
+
+ verify(tree).setProperty(eq("name"), argThat(isIterableContaining(ISO8601.format(calendar))), eq(Type.DATES));
+ }
+
+ @Test
+ public void testSetDecimalProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toDecimal(BigDecimal.ONE)).apply(root);
+
+ verify(tree).setProperty("name", BigDecimal.ONE, Type.DECIMAL);
+ }
+
+ @Test
+ public void testSetMultiDecimalProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toMultiDecimal(singletonList(BigDecimal.ONE))).apply(root);
+
+ verify(tree).setProperty(eq("name"), argThat(isIterableContaining(BigDecimal.ONE)), eq(Type.DECIMALS));
+ }
+
+ @Test
+ public void testSetDoubleProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toDouble(4.2)).apply(root);
+
+ verify(tree).setProperty("name", 4.2, Type.DOUBLE);
+ }
+
+ @Test
+ public void testSetMultiDoubleProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toMultiDouble(singletonList(4.2))).apply(root);
+
+ verify(tree).setProperty(eq("name"), argThat(isIterableContaining(4.2)), eq(Type.DOUBLES));
+ }
+
+ @Test
+ public void testSetLongProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toLong(42L)).apply(root);
+
+ verify(tree).setProperty("name", 42L, Type.LONG);
+ }
+
+ @Test
+ public void testSetMultiLongProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toMultiLong(singletonList(42L))).apply(root);
+
+ verify(tree).setProperty(eq("name"), argThat(isIterableContaining(42L)), eq(Type.LONGS));
+ }
+
+ @Test
+ public void testSetNameProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toName("value")).apply(root);
+
+ verify(tree).setProperty("name", "value", Type.NAME);
+ }
+
+ @Test
+ public void testSetMultiNameProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toMultiName(singletonList("value"))).apply(root);
+
+ verify(tree).setProperty(eq("name"), argThat(isIterableContaining("value")), eq(Type.NAMES));
+ }
+
+ @Test
+ public void testSetPathProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toPath("value")).apply(root);
+
+ verify(tree).setProperty("name", "value", Type.PATH);
+ }
+
+ @Test
+ public void testSetMultiPathProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toMultiPath(singletonList("value"))).apply(root);
+
+ verify(tree).setProperty(eq("name"), argThat(isIterableContaining("value")), eq(Type.PATHS));
+ }
+
+ @Test
+ public void testSetReferenceProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toReference("value")).apply(root);
+
+ verify(tree).setProperty("name", "value", Type.REFERENCE);
+ }
+
+ @Test
+ public void testSetMultiReferenceProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toMultiReference(singletonList("value"))).apply(root);
+
+ verify(tree).setProperty(eq("name"), argThat(isIterableContaining("value")), eq(Type.REFERENCES));
+ }
+
+ @Test
+ public void testSetStringProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toText("value")).apply(root);
+
+ verify(tree).setProperty("name", "value", Type.STRING);
+ }
+
+ @Test
+ public void testSetMultiStringProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toMultiText(singletonList("value"))).apply(root);
+
+ verify(tree).setProperty(eq("name"), argThat(isIterableContaining("value")), eq(Type.STRINGS));
+ }
+
+ @Test
+ public void testSetUriProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toUri("value")).apply(root);
+
+ verify(tree).setProperty("name", "value", Type.URI);
+ }
+
+ @Test
+ public void testSetMultiUriProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toMultiUri(singletonList("value"))).apply(root);
+
+ verify(tree).setProperty(eq("name"), argThat(isIterableContaining("value")), eq(Type.URIS));
+ }
+
+ @Test
+ public void testSetWeakReferenceProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toWeakReference("value")).apply(root);
+
+ verify(tree).setProperty("name", "value", Type.WEAKREFERENCE);
+ }
+
+ @Test
+ public void testSetMultiWeakReferenceProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name", toMultiWeakReference(singletonList("value"))).apply(root);
+
+ verify(tree).setProperty(eq("name"), argThat(isIterableContaining("value")), eq(Type.WEAKREFERENCES));
+ }
+
+}
diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/UnsetContentRemoteOperationTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/UnsetContentRemoteOperationTest.java
new file mode 100644
index 0000000..694a058
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/UnsetContentRemoteOperationTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.junit.Test;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class UnsetContentRemoteOperationTest {
+
+ private UnsetContentRemoteOperation createOperation(String path, String name) {
+ return new UnsetContentRemoteOperation(path, name);
+ }
+
+ @Test
+ public void testUnset() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+ doReturn(true).when(tree).hasProperty("name");
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name").apply(root);
+
+ verify(tree).removeProperty("name");
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testUnsetWithNonExistingTree() {
+ Tree tree = mock(Tree.class);
+ doReturn(false).when(tree).exists();
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name").apply(root);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testUnsetWithNonExistingProperty() {
+ Tree tree = mock(Tree.class);
+ doReturn(true).when(tree).exists();
+ doReturn(false).when(tree).hasProperty("name");
+
+ Root root = mock(Root.class);
+ doReturn(tree).when(root).getTree("/test");
+
+ createOperation("/test", "name").apply(root);
+ }
+
+}
diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/filter/FilterTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/filter/FilterTest.java
new file mode 100644
index 0000000..34fbd74
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/filter/FilterTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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 org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class FilterTest {
+
+ @Test
+ public void testExact() {
+ Filter filter = new Filter("name");
+
+ assertTrue(filter.matches("name"));
+ assertFalse(filter.matches("nam"));
+ assertFalse(filter.matches("named"));
+ }
+
+ @Test
+ public void testWildcard() {
+ Filter filter = new Filter("na*e");
+
+ assertTrue(filter.matches("nae"));
+ assertTrue(filter.matches("name"));
+ assertTrue(filter.matches("namme"));
+ assertFalse(filter.matches("nam"));
+ assertFalse(filter.matches("named"));
+ }
+
+ @Test
+ public void testEscapedWildcard() {
+ Filter filter = new Filter("na\\*e");
+
+ assertTrue(filter.matches("na*e"));
+ assertFalse(filter.matches("nae"));
+ assertFalse(filter.matches("name"));
+ assertFalse(filter.matches("namme"));
+ }
+
+}
diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/filter/FiltersTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/filter/FiltersTest.java
new file mode 100644
index 0000000..1a75d1d
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/filter/FiltersTest.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.filter;
+
+import com.google.common.collect.Sets;
+import org.junit.Test;
+
+import java.util.HashSet;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class FiltersTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testNullFilters() {
+ new Filters(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testNullFilter() {
+ new Filters(Sets.newHashSet((String) null));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testEmptyIncludeFilter() {
+ new Filters(Sets.newHashSet(""));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testEmptyExcludeFilter() {
+ new Filters(Sets.newHashSet("-"));
+ }
+
+ @Test
+ public void testEmptyFilters() {
+ Filters filters = new Filters(new HashSet());
+
+ assertTrue(filters.matches("foo"));
+ assertTrue(filters.matches("bar"));
+ assertTrue(filters.matches("baz"));
+ }
+
+ @Test
+ public void testIncludeFilter() {
+ Filters filters = new Filters(Sets.newHashSet("ba*"));
+
+ assertFalse(filters.matches("foo"));
+ assertTrue(filters.matches("bar"));
+ assertTrue(filters.matches("baz"));
+ }
+
+ @Test
+ public void testExcludeFilter() {
+ Filters filters = new Filters(Sets.newHashSet("-foo"));
+
+ assertFalse(filters.matches("foo"));
+ assertTrue(filters.matches("bar"));
+ assertTrue(filters.matches("baz"));
+ }
+
+ @Test
+ public void testIncludeExcludeFilters() {
+ Filters filters = new Filters(Sets.newHashSet("ba*", "-baz"));
+
+ assertFalse(filters.matches("foo"));
+ assertTrue(filters.matches("bar"));
+ assertFalse(filters.matches("baz"));
+ }
+
+}
diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/handler/AuthenticationWrapperHandlerTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/handler/AuthenticationWrapperHandlerTest.java
new file mode 100644
index 0000000..408c5df
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/handler/AuthenticationWrapperHandlerTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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.RemoteRepository;
+import org.apache.jackrabbit.oak.remote.RemoteSession;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+public class AuthenticationWrapperHandlerTest {
+
+ private Handler createHandler(Handler authenticated, Handler notAuthenticated) {
+ return new AuthenticationWrapperHandler(authenticated, notAuthenticated);
+ }
+
+ @Test
+ public void testExistingSession() throws Exception {
+ RemoteSession session = mock(RemoteSession.class);
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ doReturn(session).when(request).getAttribute("session");
+
+ HttpServletResponse response = mock(HttpServletResponse.class);
+
+ Handler authenticated = mock(Handler.class);
+
+ Handler notAuthenticated = mock(Handler.class);
+
+ createHandler(authenticated, notAuthenticated).handle(request, response);
+
+ verify(authenticated).handle(request, response);
+ verify(notAuthenticated, never()).handle(request, response);
+ }
+
+ @Test
+ public void testRepositoryNotProvided() throws Exception {
+ Handler authenticated = mock(Handler.class);
+
+ Handler notAuthenticated = mock(Handler.class);
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+
+ HttpServletResponse response = mock(HttpServletResponse.class);
+
+ createHandler(authenticated, notAuthenticated).handle(request, response);
+
+ verify(authenticated, never()).handle(request, response);
+ verify(notAuthenticated).handle(request, response);
+ }
+
+ @Test
+ public void testAuthorizationHeaderNotProvided() throws Exception {
+ Handler authenticated = mock(Handler.class);
+
+ Handler notAuthenticated = mock(Handler.class);
+
+ RemoteRepository repository = mock(RemoteRepository.class);
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ doReturn(repository).when(request).getAttribute("repository");
+
+ HttpServletResponse response = mock(HttpServletResponse.class);
+
+ createHandler(authenticated, notAuthenticated).handle(request, response);
+
+ verify(authenticated, never()).handle(request, response);
+ verify(notAuthenticated).handle(request, response);
+ }
+
+ @Test
+ public void testAuthorizationWithNonBasicScheme() throws Exception {
+ Handler authenticated = mock(Handler.class);
+
+ Handler notAuthenticated = mock(Handler.class);
+
+ RemoteRepository repository = mock(RemoteRepository.class);
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ doReturn(repository).when(request).getAttribute("repository");
+ doReturn("Whatever").when(request).getHeader("Authorization");
+
+ HttpServletResponse response = mock(HttpServletResponse.class);
+
+ createHandler(authenticated, notAuthenticated).handle(request, response);
+
+ verify(authenticated, never()).handle(request, response);
+ verify(notAuthenticated).handle(request, response);
+ }
+
+ @Test
+ public void testBasicAuthorizationWithInvalidToken() throws Exception {
+ Handler authenticated = mock(Handler.class);
+
+ Handler notAuthenticated = mock(Handler.class);
+
+ RemoteRepository repository = mock(RemoteRepository.class);
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ doReturn(repository).when(request).getAttribute("repository");
+ doReturn("Basic RG9uJ3QgeW91IGhhdmUgYW55dGhpbmcgYmV0dGVyIHRvIGRvPw==").when(request).getHeader("Authorization");
+
+ HttpServletResponse response = mock(HttpServletResponse.class);
+
+ createHandler(authenticated, notAuthenticated).handle(request, response);
+
+ verify(authenticated, never()).handle(request, response);
+ verify(notAuthenticated).handle(request, response);
+ }
+
+ @Test
+ public void testBasicAuthorizationWithValidToken() throws Exception {
+ Handler authenticated = mock(Handler.class);
+
+ Handler notAuthenticated = mock(Handler.class);
+
+ RemoteSession session = mock(RemoteSession.class);
+
+ RemoteCredentials credentials = mock(RemoteCredentials.class);
+
+ RemoteRepository repository = mock(RemoteRepository.class);
+ doReturn(credentials).when(repository).createBasicCredentials("admin", "admin".toCharArray());
+ doReturn(session).when(repository).login(credentials);
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ doReturn(repository).when(request).getAttribute("repository");
+ doReturn("Basic YWRtaW46YWRtaW4=").when(request).getHeader("Authorization");
+
+ HttpServletResponse response = mock(HttpServletResponse.class);
+
+ createHandler(authenticated, notAuthenticated).handle(request, response);
+
+ verify(request).setAttribute("session", session);
+ verify(authenticated).handle(request, response);
+ verify(notAuthenticated, never()).handle(request, response);
+ }
+
+}
diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/matcher/AllMatcherTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/matcher/AllMatcherTest.java
new file mode 100644
index 0000000..3a3e535
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/matcher/AllMatcherTest.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.matcher;
+
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletRequest;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+public class AllMatcherTest {
+
+ boolean matches(boolean... results) {
+ Matcher[] matchers = new Matcher[results.length];
+
+ for (int i = 0; i < results.length; i++) {
+ matchers[i] = mock(Matcher.class);
+ }
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+
+ for (int i = 0; i < results.length; i++) {
+ doReturn(results[i]).when(matchers[i]).match(request);
+ }
+
+ return new AllMatcher(matchers).match(request);
+
+ }
+
+ @Test
+ public void testNoMatchers() {
+ assertTrue(matches());
+ }
+
+ @Test
+ public void testSingleMatcher() {
+ assertFalse(matches(false));
+ assertTrue(matches(true));
+ }
+
+ @Test
+ public void testMatchers() {
+ assertFalse(matches(false, false));
+ assertFalse(matches(false, true));
+ assertFalse(matches(true, false));
+ assertTrue(matches(true, true));
+ }
+
+
+}
diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/matcher/MatchersTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/matcher/MatchersTest.java
new file mode 100644
index 0000000..16037a5
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/matcher/MatchersTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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 org.junit.Test;
+
+import static org.apache.jackrabbit.oak.remote.http.matcher.Matchers.matchesAll;
+import static org.apache.jackrabbit.oak.remote.http.matcher.Matchers.matchesMethod;
+import static org.apache.jackrabbit.oak.remote.http.matcher.Matchers.matchesPath;
+import static org.apache.jackrabbit.oak.remote.http.matcher.Matchers.matchesRequest;
+import static org.junit.Assert.assertNotNull;
+
+public class MatchersTest {
+
+ @Test
+ public void testCreateMethodMatcher() {
+ assertNotNull(matchesMethod("GET"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateMethodMatcherWithNullMethod() {
+ matchesMethod(null);
+ }
+
+ @Test
+ public void testCreatePathMatcher() {
+ assertNotNull(matchesPath("/test"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreatePathMatcherWithNullPattern() {
+ matchesPath(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreatePathMatcherWithInvalidPattern() {
+ matchesPath("/test(");
+ }
+
+ @Test
+ public void testCreateAllMatcher() {
+ assertNotNull(matchesAll());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateAllMatcherWithNullMatchers() {
+ matchesAll(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateAllMatchersWithNullMatcher() {
+ matchesAll(null, null);
+ }
+
+ @Test
+ public void testCreateRequestMatcher() {
+ assertNotNull(matchesRequest("GET", "/test"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateRequestMatcherWithNullMethod() {
+ matchesRequest(null, "/test");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateRequestMatcherWithNullPattern() {
+ matchesRequest("GET", null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateRequestMatcherWithInvalidPattern() {
+ matchesRequest("GET", "/test(");
+ }
+
+}
diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/matcher/MethodMatcherTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/matcher/MethodMatcherTest.java
new file mode 100644
index 0000000..8ae376d
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/matcher/MethodMatcherTest.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.http.matcher;
+
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletRequest;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+public class MethodMatcherTest {
+
+ private boolean matches(String requestMethod, String matcherMethod) {
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ doReturn(requestMethod).when(request).getMethod();
+ return new MethodMatcher(matcherMethod).match(request);
+ }
+
+ @Test
+ public void testMatch() {
+ assertTrue(matches("GET", "GET"));
+ }
+
+ @Test
+ public void testNoMatch() {
+ assertFalse(matches("GET", "PUT"));
+ }
+
+ @Test
+ public void testCaseInsensitiveMatch() {
+ assertTrue(matches("GET", "get"));
+ }
+
+}
diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/matcher/PathMatcherTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/matcher/PathMatcherTest.java
new file mode 100644
index 0000000..b569009
--- /dev/null
+++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/matcher/PathMatcherTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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 org.junit.Test;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.regex.Pattern;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+public class PathMatcherTest {
+
+ private boolean matches(String requestPath, String matcherPattern) {
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ doReturn(requestPath).when(request).getPathInfo();
+ return new PathMatcher(Pattern.compile(matcherPattern)).match(request);
+ }
+
+ @Test
+ public void testMatch() {
+ assertTrue(matches("/test", "/test"));
+ }
+
+ @Test
+ public void testCaseSensitiveMatch() {
+ assertFalse(matches("/test", "/Test"));
+ }
+
+ @Test
+ public void testPatternMatch() {
+ assertTrue(matches("/test/something", "/test/.*"));
+ }
+
+ @Test
+ public void testNoPathInfo() {
+ assertFalse(matches(null, "/test"));
+ }
+
+}
diff --git oak-run/pom.xml oak-run/pom.xml
index 8469e77..b7eb83c 100644
--- oak-run/pom.xml
+++ oak-run/pom.xml
@@ -245,6 +245,11 @@
oak-http
${project.version}
+
+ org.apache.jackrabbit
+ oak-remote
+ ${project.version}
+
org.apache.jackrabbit
oak-upgrade
diff --git oak-run/src/main/java/org/apache/jackrabbit/oak/run/Main.java oak-run/src/main/java/org/apache/jackrabbit/oak/run/Main.java
index 8aae0dc..8d88fe3 100644
--- oak-run/src/main/java/org/apache/jackrabbit/oak/run/Main.java
+++ oak-run/src/main/java/org/apache/jackrabbit/oak/run/Main.java
@@ -101,6 +101,8 @@ import org.apache.jackrabbit.oak.plugins.segment.compaction.CompactionStrategy.C
import org.apache.jackrabbit.oak.plugins.segment.file.FileStore;
import org.apache.jackrabbit.oak.plugins.segment.standby.client.StandbyClient;
import org.apache.jackrabbit.oak.plugins.segment.standby.server.StandbyServer;
+import org.apache.jackrabbit.oak.remote.content.ContentRemoteRepository;
+import org.apache.jackrabbit.oak.remote.http.RemoteServlet;
import org.apache.jackrabbit.oak.scalability.ScalabilityRunner;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.spi.state.NodeStore;
@@ -1153,6 +1155,9 @@ public class Main {
ServletHolder holder = new ServletHolder(new OakServlet(repository));
context.addServlet(holder, path + "/*");
+ ServletHolder remoteServlet = new ServletHolder(new RemoteServlet(new ContentRemoteRepository(repository)));
+ context.addServlet(remoteServlet, path + "/api/*");
+
// 2 - Webdav Server on JCR repository
final Repository jcrRepository = jcr.createRepository();
@SuppressWarnings("serial")
diff --git pom.xml pom.xml
index 9ec001f..dfdd973 100644
--- pom.xml
+++ pom.xml
@@ -56,6 +56,7 @@
oak-it
oak-pojosr
oak-authorization-cug
+ oak-remote