diff --git oak-remote/pom.xml oak-remote/pom.xml new file mode 100644 index 0000000..8e112f3 --- /dev/null +++ oak-remote/pom.xml @@ -0,0 +1,203 @@ + + + + + + 4.0.0 + + + org.apache.jackrabbit + oak-parent + 1.4-SNAPSHOT + ../oak-parent/pom.xml + + + oak-remote + Oak Remote API + bundle + + + MEMORY_NS,SEGMENT_MK + + + + + org.osgi + org.osgi.compendium + provided + + + com.google.guava + guava + provided + + + org.slf4j + slf4j-api + provided + + + javax.jcr + jcr + 2.0 + provided + + + org.apache.jackrabbit + oak-core + ${project.version} + provided + + + org.apache.jackrabbit + oak-jcr + ${project.version} + provided + + + org.apache.jackrabbit + oak-commons + ${project.version} + provided + + + org.apache.jackrabbit + jackrabbit-jcr-commons + 2.9.0 + provided + + + com.fasterxml.jackson.core + jackson-core + 2.0.0 + provided + + + com.fasterxml.jackson.core + jackson-databind + 2.0.0 + provided + + + javax.servlet + javax.servlet-api + 3.0.1 + provided + + + org.eclipse.jetty + jetty-server + 8.1.10.v20130312 + test + + + org.eclipse.jetty + jetty-servlet + 8.1.10.v20130312 + test + + + junit + junit + 4.12 + test + + + org.hamcrest + hamcrest-core + 1.3 + test + + + org.mockito + mockito-all + 1.10.19 + test + + + com.mashape.unirest + unirest-java + 1.3.27 + test + + + org.apache.jackrabbit + oak-core + ${project.version} + test-jar + test + + + org.apache.jackrabbit + oak-commons + ${project.version} + test-jar + test + + + org.mongodb + mongo-java-driver + test + + + org.slf4j + slf4j-nop + ${slf4j.version} + test + + + org.apache.felix + org.apache.felix.scr.annotations + 1.9.10 + compile + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.18.1 + + + + integration-test + verify + + + + + + org.apache.felix + maven-bundle-plugin + + + + + + + + org.apache.felix + maven-scr-plugin + + + + \ No newline at end of file diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteBinaryFilters.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteBinaryFilters.java new file mode 100644 index 0000000..13bec14 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteBinaryFilters.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote; + +/** + * Represents a set of filters that can be applied when a binary object is read + * from the repository. + */ +public class RemoteBinaryFilters { + + /** + * Return the starting offset into the binary object. This method returns + * {@code 0} by default, meaning that the binary object should be read from + * the beginning. + */ + public long getStart() { + return 0; + } + + /** + * Return the number of bytes to read. This method returns {@code -1} by + * default, meaning that the binary object should be read until the end. + */ + public long getCount() { + return -1; + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteBinaryId.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteBinaryId.java new file mode 100644 index 0000000..30f1d6f --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteBinaryId.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote; + +/** + * Represents the identifier of a binary object. + *

+ * Binary objects stored in the repository are immutable. The same binary ID is + * guaranteed to return the same binary object over time. + */ +public interface RemoteBinaryId { + + /** + * Returns a string representation of the binary ID. This representation can + * be used as a reference by an external system and converted to a {@code + * RemoteBinaryId} object using {@link RemoteSession#readBinaryId(String)}. + * + * @return A string representation of the binary ID. + */ + String asString(); + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteCommitException.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteCommitException.java new file mode 100644 index 0000000..0a0b2d1 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteCommitException.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote; + +/** + * Represents an error condition caused from a set of changes that can't be + * committed into the repository. + */ +public class RemoteCommitException extends Exception { + + public RemoteCommitException() { + } + + public RemoteCommitException(String s) { + super(s); + } + + public RemoteCommitException(String s, Throwable throwable) { + super(s, throwable); + } + + public RemoteCommitException(Throwable throwable) { + super(throwable); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteCredentials.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteCredentials.java new file mode 100644 index 0000000..1eec995 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteCredentials.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote; + +/** + * This interface is a marker interface to be used to represent both an + * authentication strategy and the information to enable that authentication + * strategy. + *

+ * In example, a {@code RemoteCredentials} object created to represent an + * authentication strategy based on user name and password, may encapsulate the + * user name and password to enable this authentication strategy when a session + * to the repository is created. + *

+ * To create instances of this interface, take a look at the methods defined in + * {@link RemoteRepository}. + */ +public interface RemoteCredentials { + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteLoginException.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteLoginException.java new file mode 100644 index 0000000..a8d245d --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteLoginException.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote; + +/** + * Represents an error condition generated by an unsuccessful login. + */ +public class RemoteLoginException extends Exception { + + public RemoteLoginException() { + } + + public RemoteLoginException(String s) { + super(s); + } + + public RemoteLoginException(String s, Throwable throwable) { + super(s, throwable); + } + + public RemoteLoginException(Throwable throwable) { + super(throwable); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteOperation.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteOperation.java new file mode 100644 index 0000000..6080c26 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteOperation.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote; + +/** + * An operation to modify content inside the repository. + *

+ * This is just a marker interface. Factory methods for {@code RemoteOperation} + * instances are found in {@link RemoteSession}. The interface doesn't currently + * expose any method, as its instances are supposed to be consumed by the same + * {@code RemoteSession} (or by another instance of the same implementation) + * that created them. + */ +public interface RemoteOperation { +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteQueryParseException.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteQueryParseException.java new file mode 100644 index 0000000..9553c4a --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteQueryParseException.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote; + +/** + * An error thrown if a query can't be correctly parsed. + */ +public class RemoteQueryParseException extends Exception { + + public RemoteQueryParseException() { + } + + public RemoteQueryParseException(String s) { + super(s); + } + + public RemoteQueryParseException(String s, Throwable throwable) { + super(s, throwable); + } + + public RemoteQueryParseException(Throwable throwable) { + super(throwable); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteRepository.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteRepository.java new file mode 100644 index 0000000..d768479 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteRepository.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote; + +import java.util.Set; + +/** + * The remote interface exposed by a repository. + *

+ * Most methods require authentication to be used. As such, a client of this + * interface is required to create an instance of {@code RemoteCredentials} and + * use this instance to login into the repository and obtain a {@code + * RemoteSession} object. + */ +public interface RemoteRepository { + + /** + * Create a {@code RemoteCredentials} object representing an authentication + * strategy based on a user name and a password. This kind of credentials + * delegates authentication to the repository, using a user identified by + * the user name and password provided to this method. + * + * @param user User name. + * @param password Password. + * @return A {@code RemoteCredentials} object representing an authentication + * strategy based on a user name and password. + */ + RemoteCredentials createBasicCredentials(String user, char[] password); + + /** + * Create a {@code RemoteCredentials} object representing an impersonation + * authentication strategy. If this authentication strategy is used, the + * repository will not make any attempt to perform authentication. It will + * instead trust the information provided by the {@code RemoteCredentials} + * and will create a {@code RemoteSession} bound to the principals specified + * to this method. + * + * @param principals The set of principals to impersonate into. + * @return A {@code RemoteCredentials} object representing an authentication + * strategy based on impersonation. + */ + RemoteCredentials createImpersonationCredentials(Set principals); + + /** + * Create a remote session exposing some repository operations. + * + * @param credentials An object representing an authentication strategy to + * be used when invoking repository operations. + * @return An instance of a remote session to invoke repository operations. + * @throws RemoteLoginException if it was not possible to authenticate the + * given credentials. + */ + RemoteSession login(RemoteCredentials credentials) throws RemoteLoginException; + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteResult.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteResult.java new file mode 100644 index 0000000..0b0442d --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteResult.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote; + +/** + * A single search result. + */ +public interface RemoteResult { + + /** + * The value attached to this search result at the specified column. + * + * @param column Name of the column. + * @return An instance of {@code RemoteValue}. + */ + RemoteValue getColumnValue(String column); + + /** + * Read the path of the node associated with the specified selector. + * + * @param selector The name of the selector. + * @return A node path. + */ + String getSelectorPath(String selector); + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteResults.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteResults.java new file mode 100644 index 0000000..4914d90 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteResults.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote; + +/** + * A collection of search results. + */ +public interface RemoteResults extends Iterable { + + /** + * If available, it returns the number of results that the query is able to + * return. The number of results is independent of the offset and limit + * options used when executing the query. + * + * @return The total number of results, or -1 if this information is not + * available. + */ + long getTotal(); + + /** + * The name of the columns contained in the search result. + * + * @return An instance of {@code Iterable}, where each element represents + * the name of a column in the search result. + */ + Iterable getColumns(); + + /** + * The name of the selectors involved in the query. + * + * @return An instance of {@code Iterable}, where each element represents + * the name of a selector involved in the query. + */ + Iterable getSelectors(); + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteRevision.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteRevision.java new file mode 100644 index 0000000..bd1e954 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteRevision.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote; + +/** + * A repository revision represents an immutable state of the repository. + *

+ * Having a revision allows you to perform repeatable reads, because it is + * assured that the state referenced by the revision is immutable. + *

+ * The revision also allows the system to reference a known state of the + * repository when changes are committed. Since a revision references an + * immutable state, committing some changes will create a new state of the + * repository that will be referenced by a new revision. + */ +public interface RemoteRevision { + + /** + * Returns a string representation of the revision. This representation can + * be used as a reference by an external system and converted to a {@code + * RemoteRevision} object using {@link RemoteSession#readRevision(String)}. + * + * @return A string representation of the revision. + */ + String asString(); + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteSession.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteSession.java new file mode 100644 index 0000000..521eb0d --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteSession.java @@ -0,0 +1,227 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote; + +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +/** + * Collection of operations available on a remote repository once the user + * correctly logged in. + *

+ * Operations working on pure content, like reading a tree or committing + * changes, requires a revision. A revision represents a snapshot of the + * repository in a specified point in time. This concept enables repeatable + * reads and consistent writes. + *

+ * When binary data is involved, this interface exposes methods to read and + * write arbitrary binary data from and to the repository. A binary data is + * considered an immutable collection of bytes that can be referenced by an + * identifier. + */ +public interface RemoteSession { + + /** + * Read the latest revision in the repository. + *

+ * This operation is always meant to succeed, because the repository will + * always have an initial revision to return to the caller. + * + * @return The latest revision in the repository. + */ + RemoteRevision readLastRevision(); + + /** + * Read a revision given a string representation of the revision itself. + *

+ * This operation may fail for a number of reasons. In example, the string + * passed to this method is not a valid revision, or this string represents + * a revision that was valid in the past but it is no more valid. + * + * @param revision The string representation of the revision. + * @return The revision represented by the string passed to this method, or + * {@code null} if the string representation is invalid. + */ + RemoteRevision readRevision(String revision); + + /** + * Read a sub-tree from the repository at the given revision. Some filters + * may be applied to the tree to avoid reading unwanted information. + * + * @param revision The revision representing the state of the repository to + * read from. + * @param path The path of the root of the subtree to read. + * @param filters Filters to apply to the returned tree. + * @return The tree requested by the given path, filtered according to the + * provided filters. The method can return {@code null} if the root of the + * tree is not found in the repository for the given revision. + */ + RemoteTree readTree(RemoteRevision revision, String path, RemoteTreeFilters filters); + + /** + * Create an operation to represent the addition of a new node in the + * repository. + * + * @param path Path of the new node to create. + * @param properties Initial set of properties attached to the new node. + * @return An operation representing the addition of a new node. + */ + RemoteOperation createAddOperation(String path, Map properties); + + /** + * Create an operation representing the removal of an existing node from the + * repository. + * + * @param path Path of the node to remove. + * @return An operation representing the removal of an existing node. + */ + RemoteOperation createRemoveOperation(String path); + + /** + * Create an operation representing the creation or modification of a + * property of an existing node. + * + * @param path Path of the node where the property is or will be attached + * to. + * @param name Name of the property to set. + * @param value Value of the property. + * @return An operation representing the creation or modification of a + * property of an existing node. + */ + RemoteOperation createSetOperation(String path, String name, RemoteValue value); + + /** + * Create an operation to represent the removal of an existing property from + * an existing node in the repository. + * + * @param path Path of the node where the property is attached to. + * @param name Name of the property to remove. + * @return An operation representing the removal of a property. + */ + RemoteOperation createUnsetOperation(String path, String name); + + /** + * Create an operation to represent the copy of a subtree into another + * location into the repository. + * + * @param source Path of the root of the subtree to copy. + * @param target Path where the subtree should be copied to. + * @return An operation representing a copy of a subtree. + */ + RemoteOperation createCopyOperation(String source, String target); + + /** + * Create an operation to represent the move of a subtree into another + * location into the repository. + * + * @param source Path of the root of the source subtree to move. + * @param target Path where the subtree should be moved to. + * @return An operation representing a move of a subtree. + */ + RemoteOperation createMoveOperation(String source, String target); + + /** + * Create an operation that represents the aggregation of multiple, simpler + * operations. The aggregated operations are applied in the same sequence + * provided by this method. + * + * @param operations Sequence of operations to aggregate. + * @return An operation that, when executed, will execute the provided + * operations in the provided sequence. + */ + RemoteOperation createAggregateOperation(List operations); + + /** + * Commit some changes to the repository. The changes are represented by an + * operation. The operation will be applied on the repository state + * represented by the given revision. + * + * @param revision Revision where the changes should be applied to. + * @param operation Operation to change the state of the repository. + * @return A new revision representing the new state of the repository where + * the changes are applied. + * @throws RemoteCommitException if the provided operation can't be + * performed on the provided repository + * state. + */ + RemoteRevision commit(RemoteRevision revision, RemoteOperation operation) throws RemoteCommitException; + + /** + * Read a binary ID given a string representation of the binary ID itself. + *

+ * This operations may fail for a number of reasons. In example, the string + * doesn't represent a valid binary ID, or the string represents a binary ID + * that was valid in the past but is no more valid. + * + * @param binaryId String representation of the binary ID. + * @return The binary ID read from the repository. This method may return + * {@code null} if the string representation of the binary ID is not valid. + */ + RemoteBinaryId readBinaryId(String binaryId); + + /** + * Read a binary object from the repository according to the given filters. + *

+ * In the case of a binary object, filters are really simple. At most, it is + * possible to read just a portion of the binary object instead of reading + * it in its entirety. + * + * @param binaryId Binary ID referring to the binary object to read. + * @param filters Filters to apply to the returned binary object. + * @return A stream representing the filtered content of the binary object. + */ + InputStream readBinary(RemoteBinaryId binaryId, RemoteBinaryFilters filters); + + /** + * Read the length of a binary object from the repository. + * + * @param binaryId Binary ID referring to the binary object whose length + * should be read. + * @return The length of the binary object. + */ + long readBinaryLength(RemoteBinaryId binaryId); + + /** + * Write a binary object into the repository and return a binary ID + * referencing to it. + * + * @param stream Stream representing the binary object to write. + * @return Binary ID referencing the binary object written in the + * repository. + */ + RemoteBinaryId writeBinary(InputStream stream); + + /** + * Performs a search in the content and returns a set of search results. + * + * @param revision The revision that should be used when searching the + * content. + * @param query The query. It may contain placeholders that are to be + * substituted with the actual parameters. + * @param language The language the query is written in. It identifies the + * syntax of the query. + * @param offset How many rows to skip when returning the results. + * @param limit How many results to return. + * @return Search results. + * @throws RemoteQueryParseException if the query can't be correctly + * parsed. + */ + RemoteResults search(RemoteRevision revision, String query, String language, long offset, long limit) throws RemoteQueryParseException; + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteTree.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteTree.java new file mode 100644 index 0000000..0b8c37b --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteTree.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote; + +import java.util.Map; + +/** + * This interface is a recursive data structure representing a view on a + * repository tree. + *

+ * The purpose of the remote tree is not to represent exactly the content as + * stored in the repository, but to provide a view on that content fulfilling + * the filtering options provided by the client when the tree was accessed. + */ +public interface RemoteTree { + + /** + * Read the properties associated to the root of this remote tree. The root + * of this remote tree is represented by the instance of {@code RemoteTree} + * this method is invoked on. + * + * @return the properties associated to the root of this remote tree. + */ + Map getProperties(); + + /** + * Read the children associated to the root of this remote tree. The root of + * this remote tree is represented by the instance of {@code RemoteTree} + * this method is invoked on. The children of this remote tree are + * themselves remote trees. + *

+ * The remote tree may be truncated at some point (e.g. to avoid very deep + * remote trees to be returned), and this is the reason why the values of + * this {@code Map} can be {@code null}. When a {@code null} value is met, + * the consumer of this interface must assume that there is another subtree + * rooted under the corresponding key, but it is not returned to fulfill the + * filtering options provided when this tree was read. + * + * @return The children associated to the root of this remote tree. + */ + Map getChildren(); + + /** + * Return a flag to indicate that this remote tree actually has more + * children than the one returned by {@link #getChildren()}. + *

+ * This flag is important when the repository tree is read using very strict + * filtering options regarding the maximum number of children to return. If + * this method returns {@code true}, a consumer of this interface must + * assume that there are more children than the one attached to the root of + * this tree. They could be retrieved by varying the relevant filtering + * options and performing another read for this subtree. + * + * @return {@true} if this remote tree is not exposing the full set of + * children as stored in the repository, {@code false} otherwise. + */ + boolean hasMoreChildren(); + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteTreeFilters.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteTreeFilters.java new file mode 100644 index 0000000..860a7d3 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteTreeFilters.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote; + +import java.util.Set; + +import static com.google.common.collect.Sets.newHashSet; + +/** + * Represents a set of filters that can be applied when a subtree is read from + * the repository. + */ +public class RemoteTreeFilters { + + /** + * Return the depth of the tree to read. This method returns {@code 0} by + * default, meaning that only the root of the subtree will be returned. The + * default value makes read operation for a subtree look like the read + * operation for a single node. + */ + public int getDepth() { + return 0; + } + + /** + * Return the property filters. This method returns {@code {"*"}} by + * default, meaning that every property will be returned for every node + * included in the subtree. + */ + public Set getPropertyFilters() { + return newHashSet("*"); + } + + /** + * Return the node filters. This method returns a value of {@code {"*"}} by + * default, meaning that every descendant node of the root of the tree wil + * be returned. + */ + public Set getNodeFilters() { + return newHashSet("*"); + } + + /** + * Return the binary threshold. This method returns {@code 0} by default, + * meaning that by default binary properties will be returned as references + * to binary objects, instead of being returned as proper binary objects. + */ + public long getBinaryThreshold() { + return 0; + } + + /** + * Return the start index for children. This method returns {@code 0} by + * default, meaning that children will be read from the beginning when + * reading the root node and every descendant. + */ + public int getChildrenStart() { + return 0; + } + + /** + * Return the maximum number of children to return. This method returns + * {@code -1} by default, meaning that every children will be returned when + * reading the root node and every descendant. + */ + public int getChildrenCount() { + return -1; + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteValue.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteValue.java new file mode 100644 index 0000000..321408d --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/RemoteValue.java @@ -0,0 +1,1431 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote; + +import java.io.InputStream; +import java.math.BigDecimal; + +/** + * Represents a value that can be assigned to a property in the repository. + * Client of the remote repository provides values as instances of this class. + */ +public class RemoteValue { + + /** + * A generic interface to represent a supplier of an item. + *

+ * In the specific, it is used by values whose underlying implementation is + * an {@code InputStream}. To enable multiple traversals of {@code + * InputStream}s, the value is wrapped by this interface to effectively have + * a factory over the underlying {@code InputStream}. + * + * @param Type of the item this object is able to create. + */ + public static interface Supplier { + + T get(); + + } + + /** + * This class helps executing logic that depends on the type of a remote + * value. Instead of manually branching code depending on the result of + * {@code isText}, {@code isBoolean} and so on, a handler can be implemented + * to provide different logic for different types. + */ + public static class TypeHandler { + + public void isBinary(Supplier value) { + throw new IllegalStateException("case not handled"); + } + + public void isMultiBinary(Iterable> value) { + throw new IllegalStateException("case not handled"); + } + + public void isBinaryId(String value) { + throw new IllegalStateException("case not handled"); + } + + public void isMultiBinaryId(Iterable value) { + throw new IllegalStateException("case not handled"); + } + + public void isBoolean(Boolean value) { + throw new IllegalStateException("case not handled"); + } + + public void isMultiBoolean(Iterable value) { + throw new IllegalStateException("case not handled"); + } + + public void isDate(Long value) { + throw new IllegalStateException("case not handled"); + } + + public void isMultiDate(Iterable value) { + throw new IllegalStateException("case not handled"); + } + + public void isDecimal(BigDecimal value) { + throw new IllegalStateException("case not handled"); + } + + public void isMultiDecimal(Iterable value) { + throw new IllegalStateException("case not handled"); + } + + public void isDouble(Double value) { + throw new IllegalStateException("case not handled"); + } + + public void isMultiDouble(Iterable value) { + throw new IllegalStateException("case not handled"); + } + + public void isLong(Long value) { + throw new IllegalStateException("case not handled"); + } + + public void isMultiLong(Iterable value) { + throw new IllegalStateException("case not handled"); + } + + public void isName(String value) { + throw new IllegalStateException("case not handled"); + } + + public void isMultiName(Iterable value) { + throw new IllegalStateException("case not handled"); + } + + public void isPath(String value) { + throw new IllegalStateException("case not handled"); + } + + public void isMultiPath(Iterable value) { + throw new IllegalStateException("case not handled"); + } + + public void isReference(String value) { + throw new IllegalStateException("case not handled"); + } + + public void isMultiReference(Iterable value) { + throw new IllegalStateException("case not handled"); + } + + public void isText(String value) { + throw new IllegalStateException("case not handled"); + } + + public void isMultiText(Iterable value) { + throw new IllegalStateException("case not handled"); + } + + public void isUri(String value) { + throw new IllegalStateException("case not handled"); + } + + public void isMultiUri(Iterable value) { + throw new IllegalStateException("case not handled"); + } + + public void isWeakReference(String value) { + throw new IllegalStateException("case not handled"); + } + + public void isMultiWeakReference(Iterable value) { + throw new IllegalStateException("case not handled"); + } + + public void isUnknown() { + throw new IllegalStateException("case not handled"); + } + } + + /** + * Create a remote value of type string. + * + * @param value The string wrapped by the remote value. + * @return A remote value of type string wrapping the provided value. + */ + public static RemoteValue toText(final String value) { + return new RemoteValue() { + + @Override + public boolean isText() { + return true; + } + + @Override + public String asText() { + return value; + } + + }; + } + + /** + * Create a remote multi-value of type string. + * + * @param value The collection of strings wrapped by the remote value. + * @return A remote multi-value of type string wrapping the provided + * collection of strings. + */ + public static RemoteValue toMultiText(final Iterable value) { + return new RemoteValue() { + + @Override + public boolean isMultiText() { + return true; + } + + @Override + public Iterable asMultiText() { + return value; + } + + }; + } + + /** + * Create a remote value of type binary. + * + * @param value The factory of input streams wrapped by the remote value. + * @return A remote value of type binary wrapping the provided factory of + * input streams. + */ + public static RemoteValue toBinary(final Supplier value) { + return new RemoteValue() { + + @Override + public boolean isBinary() { + return true; + } + + @Override + public Supplier asBinary() { + return value; + } + + }; + } + + /** + * Create a remote multi-value of type binary. + * + * @param value The collection of factories of input streams wrapped by the + * remote value. + * @return A remote multi-value of type binary wrapping the provided + * collection of input streams. + */ + public static RemoteValue toMultiBinary(final Iterable> value) { + return new RemoteValue() { + + @Override + public boolean isMultiBinary() { + return true; + } + + @Override + public Iterable> asMultiBinary() { + return value; + } + + }; + } + + /** + * Create a remote value of type binary ID. + * + * @param value The binary ID wrapped by the remote value. + * @return A remote value wrapping the provided binary ID. + */ + public static RemoteValue toBinaryId(final String value) { + return new RemoteValue() { + + @Override + public boolean isBinaryId() { + return true; + } + + @Override + public String asBinaryId() { + return value; + } + + }; + } + + /** + * Create a remote multi-value of type binary ID. + * + * @param value The collection of binary IDs wrapped by the remote value. + * @return A remote multi-value wrapping the provided collection of binary + * IDs. + */ + public static RemoteValue toMultiBinaryId(final Iterable value) { + return new RemoteValue() { + + @Override + public boolean isMultiBinaryId() { + return true; + } + + @Override + public Iterable asMultiBinaryId() { + return value; + } + + }; + } + + /** + * Create a remote value of type long. + * + * @param value The long to wrap in a remote value. + * @return A remote value of type long wrapping the provided long value. + */ + public static RemoteValue toLong(final long value) { + return new RemoteValue() { + + @Override + public boolean isLong() { + return true; + } + + + @Override + public Long asLong() { + return value; + } + + }; + } + + /** + * Create a remote multi-value of type long. + * + * @param value The collection of long values to wrap in a remote value. + * @return A remote multi-value of type long wrapping the provided + * collection of long values. + */ + public static RemoteValue toMultiLong(final Iterable value) { + return new RemoteValue() { + + @Override + public boolean isMultiLong() { + return true; + } + + @Override + public Iterable asMultiLong() { + return value; + } + + }; + } + + /** + * Create a remote value of type double. + * + * @param value The double value to wrap into a remote value. + * @return A remote value wrapping the provided remote value. + */ + public static RemoteValue toDouble(final double value) { + return new RemoteValue() { + + @Override + public boolean isDouble() { + return true; + } + + @Override + public Double asDouble() { + return value; + } + + }; + } + + /** + * Create a remote multi-value of type double. + * + * @param value The collection of double values to wrap into a remote + * value. + * @return A remote multi-value of type double wrapping the provided + * collection of double values. + */ + public static RemoteValue toMultiDouble(final Iterable value) { + return new RemoteValue() { + + @Override + public boolean isMultiDouble() { + return true; + } + + @Override + public Iterable asMultiDouble() { + return value; + } + + }; + } + + /** + * Create a remote value of type date. + * + * @param value The date to wrap into a remote value. The date is expressed + * in milliseconds since January 1, 1970, 00:00:00 GMT. + * @return A remote value of type date wrapping the provided date. + */ + public static RemoteValue toDate(final long value) { + return new RemoteValue() { + + @Override + public boolean isDate() { + return true; + } + + @Override + public Long asDate() { + return value; + } + + }; + } + + /** + * Create a remote multi-value of type date. + * + * @param value The collection of dates to wrap into a remote value. Every + * date is expressed in milliseconds since January 1, 1970, + * 00:00:00 GMT. + * @return A remote multi-value of type date wrapping the provided + * collection of dates. + */ + public static RemoteValue toMultiDate(final Iterable value) { + return new RemoteValue() { + + @Override + public boolean isMultiDate() { + return true; + } + + @Override + public Iterable asMultiDate() { + return value; + } + + }; + } + + /** + * Create a remote value of type boolean. + * + * @param value The boolean value to wrap into a remote value. + * @return A remote value wrapping the provided boolean value. + */ + public static RemoteValue toBoolean(final boolean value) { + return new RemoteValue() { + + @Override + public boolean isBoolean() { + return true; + } + + @Override + public Boolean asBoolean() { + return value; + } + + }; + } + + /** + * Create a remote multi-value of type boolean. + * + * @param value The collection of boolean values to wrap into a remote + * value. + * @return A remote value wrapping the provided collection of boolean + * values. + */ + public static RemoteValue toMultiBoolean(final Iterable value) { + return new RemoteValue() { + + @Override + public boolean isMultiBoolean() { + return true; + } + + @Override + public Iterable asMultiBoolean() { + return value; + } + + }; + } + + /** + * Create a remote value of type name. + * + * @param value The name to wrap into a remote value. A name is represented + * as a string. + * @return A remote value of type name wrapping the provided string. + */ + public static RemoteValue toName(final String value) { + return new RemoteValue() { + + @Override + public boolean isName() { + return true; + } + + @Override + public String asName() { + return value; + } + + }; + } + + /** + * Create a remote multi-value of type name. + * + * @param value The collection of names to wrap into a remote value. Every + * name is represented by a string. + * @return A remote multi-value of type name wrapping the provided + * collection of strings. + */ + public static RemoteValue toMultiName(final Iterable value) { + return new RemoteValue() { + + @Override + public boolean isMultiName() { + return true; + } + + @Override + public Iterable asMultiName() { + return value; + } + + }; + } + + /** + * Create a remote value of type path. + * + * @param value The path to wrap into the remote value. A path is + * represented by a string. + * @return A remote value of type path wrapping the provided string. + */ + public static RemoteValue toPath(final String value) { + return new RemoteValue() { + + @Override + public boolean isPath() { + return true; + } + + @Override + public String asPath() { + return value; + } + + }; + } + + /** + * Create a remote multi-value of type path. + * + * @param value The collection of paths to wrap into a remote value. Every + * path is represented by a string. + * @return A remote multi-value of type path wrapping the provided strings. + */ + public static RemoteValue toMultiPath(final Iterable value) { + return new RemoteValue() { + + @Override + public boolean isMultiPath() { + return true; + } + + @Override + public Iterable asMultiPath() { + return value; + } + + }; + } + + /** + * Create a remote value of type reference. + * + * @param value The reference to wrap in a remote value. The reference is + * represented by a string. + * @return A remote value of type reference wrapping the provided string. + */ + public static RemoteValue toReference(final String value) { + return new RemoteValue() { + + @Override + public boolean isReference() { + return true; + } + + @Override + public String asReference() { + return value; + } + + }; + } + + /** + * Create a remote multi-value of type reference. + * + * @param value The collection of references to wrap in a remote value. + * Every reference is represented by a string. + * @return A remote multi-value of type reference wrapping the provided + * collection of strings. + */ + public static RemoteValue toMultiReference(final Iterable value) { + return new RemoteValue() { + + @Override + public boolean isMultiReference() { + return true; + } + + @Override + public Iterable asMultiReference() { + return value; + } + + }; + } + + /** + * Create a remote value of type weak reference. + * + * @param value The weak reference to wrap into a remote value. The weak + * reference is represented by a string value. + * @return A remote value of type weak reference wrapping the provided + * string value. + */ + public static RemoteValue toWeakReference(final String value) { + return new RemoteValue() { + + @Override + public boolean isWeakReference() { + return true; + } + + @Override + public String asWeakReference() { + return value; + } + + }; + } + + /** + * Create a remote multi-value of type weak reference. + * + * @param value The collection of weak references to wrap into a remote + * value. Every weak reference is represented by a string. + * @return A remote multi-value of type weak reference wrapping the provided + * collection of strings. + */ + public static RemoteValue toMultiWeakReference(final Iterable value) { + return new RemoteValue() { + + @Override + public boolean isMultiWeakReference() { + return true; + } + + @Override + public Iterable asMultiWeakReference() { + return value; + } + + }; + } + + /** + * Create a remote value of type URI. + * + * @param value The string representation of the URI to wrap into a remote + * value. + * @return A remote value of type URI wrapping the provided string. + */ + public static RemoteValue toUri(final String value) { + return new RemoteValue() { + + @Override + public boolean isUri() { + return true; + } + + @Override + public String asUri() { + return value; + } + + }; + } + + /** + * Create a remote multi-value of type URI. + * + * @param value The collection of URIs to wrap into the remote value. Every + * URI is represented by a string. + * @return A remote multi-value of type URI wrapping the provided collection + * of strings. + */ + public static RemoteValue toMultiUri(final Iterable value) { + return new RemoteValue() { + + @Override + public boolean isMultiUri() { + return true; + } + + @Override + public Iterable asMultiUri() { + return value; + } + + }; + } + + /** + * Create a remote value of type decimal. + * + * @param value The decimal to wrap into a remote value. + * @return A remote value of type decimal wrapping the provided decimal + * value. + */ + public static RemoteValue toDecimal(final BigDecimal value) { + return new RemoteValue() { + + @Override + public boolean isDecimal() { + return true; + } + + @Override + public BigDecimal asDecimal() { + return value; + } + + }; + } + + /** + * Create a remote multi-value of type decimal. + * + * @param value The collection of decimals to wrap into a remote value. + * @return A remote multi-value of type decimal wrapping the provided + * collection of decimals. + */ + public static RemoteValue toMultiDecimal(final Iterable value) { + return new RemoteValue() { + + @Override + public boolean isMultiDecimal() { + return true; + } + + @Override + public Iterable asMultiDecimal() { + return value; + } + + }; + } + + private RemoteValue() { + + } + + /** + * Check if this remote value is of type string. + * + * @return {@code true} if this remote value is of type string, {@code + * false} otherwise. + */ + public boolean isText() { + return false; + } + + /** + * Read the value of this remote string value. + * + * @return The string wrapped by this remote value if this remote value is + * of type string, {@code null} otherwise. + */ + public String asText() { + return null; + } + + /** + * Check if this remote value is a multi-value of type string. + * + * @return {@code true} if this remote value is a multi-value of type + * string, {@code false} otherwise. + */ + public boolean isMultiText() { + return false; + } + + /** + * Read the value of this remote string multi-value. + * + * @return The collection of strings wrapped by this remote value if this + * remote value is a multi-value of type string, {@code null} otherwise. + */ + public Iterable asMultiText() { + return null; + } + + /** + * Check if this remote value is of type binary. + * + * @return {@code true} if this remote value is of type binary, {@code + * false} otherwise. + */ + public boolean isBinary() { + return false; + } + + /** + * Read the value of this remote boolean value. + * + * @return The value of this remote value if this remote value is of type + * binary, {@code null} otherwise. + */ + public Supplier asBinary() { + return null; + } + + /** + * Check if this remote value is a multi-value of type binary. + * + * @return {@code true} if this remote value is a multi-value of type + * binary, {@code false} otherwise. + */ + public boolean isMultiBinary() { + return false; + } + + /** + * Read the value of this remote binary multi-value. + * + * @return The value of this remote value if this remote value is a + * multi-value of type binary, {@code null} otherwise. + */ + public Iterable> asMultiBinary() { + return null; + } + + /** + * Check if this remote value is of type binary ID. + * + * @return {@code true} if this remote value is of type binary ID, {@code + * false} otherwise. + */ + public boolean isBinaryId() { + return false; + } + + /** + * Read the value of this remote binary ID multi-value. + * + * @return The value of this remote value if this remote value is of type + * binary ID, {@code null} otherwise. + */ + public String asBinaryId() { + return null; + } + + /** + * Check if this remote value is a multi-value of type binary ID. + * + * @return {@code true} if this remote value is a multi-value of type binary + * ID, {@code false} otherwise. + */ + public boolean isMultiBinaryId() { + return false; + } + + /** + * Return the value of this remote binary ID multi-value. + * + * @return The value of this remote value if this remote value is a + * multi-value of type binary ID, {@code null} otherwise. + */ + public Iterable asMultiBinaryId() { + return null; + } + + /** + * Check if this remote value is of type long. + * + * @return {@code true} if this remote value is of type long, {@code false} + * otherwise. + */ + public boolean isLong() { + return false; + } + + /** + * Read the value of this remote long multi-value. + * + * @return The value of this remote value if this remote value is of type + * long, {@code null} otherwise. + */ + public Long asLong() { + return null; + } + + /** + * Check if this remote value is a multi-value of type long. + * + * @return {@code true} if this value is a multi-value of type long, {@code + * false} otherwise. + */ + public boolean isMultiLong() { + return false; + } + + /** + * Read the value of this remote multi-value of type long. + * + * @return The value of this remote value if this remote value is a + * multi-value of type long, {@code null} otherwise. + */ + public Iterable asMultiLong() { + return null; + } + + /** + * Check if this remote value is of type double. + * + * @return {@code true} if this remote value is of type long, {@code false} + * otherwise. + */ + public boolean isDouble() { + return false; + } + + /** + * Read the value of this remote value of type double. + * + * @return The value of this remote value if this remote value is of type + * double, {@code null} otherwise. + */ + public Double asDouble() { + return null; + } + + /** + * Check if this remote value is a multi-value of type double. + * + * @return {@code true} if this remote value is a multi-value of type + * double, {@code false} otherwise. + */ + public boolean isMultiDouble() { + return false; + } + + /** + * Read the value of this remote multi-value of type double. + * + * @return The value of this remote value if this remote value is a + * multi-value of type double, {@code null} otherwise. + */ + public Iterable asMultiDouble() { + return null; + } + + /** + * Check if this remote value is of type date. + * + * @return {@code true} if this remote value is of type date, {@code false} + * otherwise. + */ + public boolean isDate() { + return false; + } + + /** + * Read the value of this remote multi-value of type date. + * + * @return The value of this remote value if this remote value is of type + * date, {@code null} otherwise. + */ + public Long asDate() { + return null; + } + + /** + * Check if this remote value is a multi-value of type date. + * + * @return {@code true} if this remote value is a multi-value of type date, + * {@code false} otherwise. + */ + public boolean isMultiDate() { + return false; + } + + /** + * Read the value of this remote multi-value of type date. + * + * @return The value of this remote value if this remote value is a + * multi-value of type date, {@code null} otherwise. + */ + public Iterable asMultiDate() { + return null; + } + + /** + * Check if this remote value is of type boolean. + * + * @return {@code true} if this remote value is fo type boolean, {@code + * false} otherwise. + */ + public boolean isBoolean() { + return false; + } + + /** + * Read the value of this remote value of type boolean. + * + * @return The value of this remote value if this remote value is of type + * boolean, false otherwise. + */ + public Boolean asBoolean() { + return null; + } + + /** + * Check if this remote value is a multi-value of type boolean. + * + * @return {@code true} if this remote value is a multi-value of type + * boolean, {@code false} otherwise. + */ + public boolean isMultiBoolean() { + return false; + } + + /** + * Read the value of this remote multi-value of type boolean. + * + * @return The value of this remote value if this remote value is a + * multi-value of type boolean, {@code null} otherwise. + */ + public Iterable asMultiBoolean() { + return null; + } + + /** + * Check if this remote value is of type name. + * + * @return {@code true} if this remote value is of type name, {@code false} + * otherwise. + */ + public boolean isName() { + return false; + } + + /** + * Read the value of this remote value of type name. + * + * @return The value of this remote value if this remote value is of type + * name, {@code null} otherwise. + */ + public String asName() { + return null; + } + + /** + * Check if this remote value is a multi-value of type name. + * + * @return {@code true} if this remote value is a multi-value of type name, + * {@code false} otherwise. + */ + public boolean isMultiName() { + return false; + } + + /** + * Read the value of this remote multi-value of type name. + * + * @return The value of this remote value if this remote value is a + * multi-value of type name, {@code null} otherwise. + */ + public Iterable asMultiName() { + return null; + } + + /** + * Check if this value is of type path. + * + * @return {@code true} if this remote value is of type path, {@code false} + * otherwise. + */ + public boolean isPath() { + return false; + } + + /** + * Read the value of this remote value of type path. + * + * @return The value of this remote value if this remote value is of type + * path, {@code null} otherwise. + */ + public String asPath() { + return null; + } + + /** + * Check if this remote value is a multi-value of type path. + * + * @return {@code true} if this remote value is a multi-value of type path, + * {@code false} otherwise. + */ + public boolean isMultiPath() { + return false; + } + + /** + * Read the value of this remote multi-value of type path. + * + * @return The value of this remote value if this remote value is a + * multi-value of type path, {@code null} otherwise. + */ + public Iterable asMultiPath() { + return null; + } + + /** + * Check if this remote value is of type reference. + * + * @return {@code true} if this remote value is of type reference, {@code + * false} otherwise. + */ + public boolean isReference() { + return false; + } + + /** + * Read the value of this remote value of type reference. + * + * @return The value of this remote value if this remote value is of type + * reference, {@code null} otherwise. + */ + public String asReference() { + return null; + } + + /** + * Check if this remote value is a multi-value of type reference. + * + * @return {@code true} if this remote value is a multi-value of type + * reference, {@code false} otherwise. + */ + public boolean isMultiReference() { + return false; + } + + /** + * Read the value of this remote multi-value of type reference. + * + * @return The value of this remote value if this remote value is a + * multi-value of type reference, {@code null} otherwise. + */ + public Iterable asMultiReference() { + return null; + } + + /** + * Check if this remote value is of type weak reference. + * + * @return {@code true} if this remote value is fo type weak reference, + * {@code false} otherwise. + */ + public boolean isWeakReference() { + return false; + } + + /** + * Read the value of this remote value of type weak reference. + * + * @return The value of this remote value if this remote value is of type + * weak reference, {@code null} otherwise. + */ + public String asWeakReference() { + return null; + } + + /** + * Check if this remote value is a multi-value of type weak reference. + * + * @return {@code true} if this remote value is a multi-value of type weak + * reference, {@code false} otherwise. + */ + public boolean isMultiWeakReference() { + return false; + } + + /** + * Read the value of this remote multi-value of type weak reference. + * + * @return The value of this remote value if this remote value is a + * multi-value of type weak reference, {@code null} otherwise. + */ + public Iterable asMultiWeakReference() { + return null; + } + + /** + * Check if this remote value is of type decimal. + * + * @return {@code true} if this remote value is of type decimal, {@code + * false} otherwise. + */ + public boolean isDecimal() { + return false; + } + + /** + * Read the value of this remote value of type decimal. + * + * @return The value of this remote value if this remote value is of type + * decimal, {@code null} otherwise. + */ + public BigDecimal asDecimal() { + return null; + } + + /** + * Check if this remote value is a multi-value of type decimal. + * + * @return {@code true} if this remote value is a multi-value of type + * decimal, {@code false} otherwise. + */ + public boolean isMultiDecimal() { + return false; + } + + /** + * Read the value of this remote multi-value of type decimal. + * + * @return The value of this remote value if this remote value is a + * multi-value of type decimal, {@code null} otherwise. + */ + public Iterable asMultiDecimal() { + return null; + } + + /** + * Check if this remote value is of type URI. + * + * @return {@code true} if this remote value is of type URI, {@code false} + * otherwise. + */ + public boolean isUri() { + return false; + } + + /** + * Read the value of this remote value of type URI. + * + * @return The value of this remote value if this remote value is of type + * URI, {@code null} otherwise. + */ + public String asUri() { + return null; + } + + /** + * Check if this remote value is a multi-value of type URI. + * + * @return {@code true} if this remote value is a multi-value of type URI, + * {@code false} otherwise. + */ + public boolean isMultiUri() { + return false; + } + + /** + * Read the value of this remote multi-value of type URI. + * + * @return The value of this remote value if this remote value is a + * multi-value of type URI, {@code null} otherwise. + */ + public Iterable asMultiUri() { + return null; + } + + /** + * Calls a method of the provided handler according to the type of this + * remote value. + * + * @param handler Handler containing logic to be executing according to the + * type of this remote value. + */ + public void whenType(TypeHandler handler) { + if (isBinary()) { + handler.isBinary(asBinary()); + return; + } + + if (isMultiBinary()) { + handler.isMultiBinary(asMultiBinary()); + return; + } + + if (isBinaryId()) { + handler.isBinaryId(asBinaryId()); + return; + } + + if (isMultiBinaryId()) { + handler.isMultiBinaryId(asMultiBinaryId()); + return; + } + + if (isBoolean()) { + handler.isBoolean(asBoolean()); + return; + } + + if (isMultiBoolean()) { + handler.isMultiBoolean(asMultiBoolean()); + return; + } + + if (isDate()) { + handler.isDate(asDate()); + return; + } + + if (isMultiDate()) { + handler.isMultiDate(asMultiDate()); + return; + } + + if (isDecimal()) { + handler.isDecimal(asDecimal()); + return; + } + + if (isMultiDecimal()) { + handler.isMultiDecimal(asMultiDecimal()); + return; + } + + if (isDouble()) { + handler.isDouble(asDouble()); + return; + } + + if (isMultiDouble()) { + handler.isMultiDouble(asMultiDouble()); + return; + } + + if (isLong()) { + handler.isLong(asLong()); + return; + } + + if (isMultiLong()) { + handler.isMultiLong(asMultiLong()); + return; + } + + if (isName()) { + handler.isName(asName()); + return; + } + + if (isMultiName()) { + handler.isMultiName(asMultiName()); + return; + } + + if (isPath()) { + handler.isPath(asPath()); + return; + } + + if (isMultiPath()) { + handler.isMultiPath(asMultiPath()); + return; + } + + if (isReference()) { + handler.isReference(asReference()); + return; + } + + if (isMultiReference()) { + handler.isMultiReference(asMultiReference()); + return; + } + + if (isText()) { + handler.isText(asText()); + return; + } + + if (isMultiText()) { + handler.isMultiText(asMultiText()); + return; + } + + if (isUri()) { + handler.isUri(asUri()); + return; + } + + if (isMultiUri()) { + handler.isMultiUri(asMultiUri()); + return; + } + + if (isWeakReference()) { + handler.isWeakReference(asWeakReference()); + return; + } + + if (isMultiWeakReference()) { + handler.isMultiWeakReference(asMultiWeakReference()); + return; + } + + handler.isUnknown(); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/AddContentRemoteOperation.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/AddContentRemoteOperation.java new file mode 100644 index 0000000..912ca5d --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/AddContentRemoteOperation.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.remote.RemoteCommitException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class AddContentRemoteOperation implements ContentRemoteOperation { + + private static final Logger logger = LoggerFactory.getLogger(AddContentRemoteOperation.class); + + private final String path; + + public AddContentRemoteOperation(String path) { + this.path = path; + } + + @Override + public void apply(Root root) throws RemoteCommitException { + logger.debug("performing 'add' operation on path={}", path); + + Tree tree = root.getTree(path); + + if (tree.exists()) { + throw new RemoteCommitException("node already exists"); + } + + Tree parent = tree.getParent(); + + if (!parent.exists()) { + throw new RemoteCommitException("parent node does not exist"); + } + + parent.addChild(tree.getName()); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/AggregateContentRemoteOperation.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/AggregateContentRemoteOperation.java new file mode 100644 index 0000000..f23ac61 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/AggregateContentRemoteOperation.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.remote.RemoteCommitException; + +import java.util.List; + +class AggregateContentRemoteOperation implements ContentRemoteOperation { + + private final List operations; + + public AggregateContentRemoteOperation(List operations) { + this.operations = operations; + } + + @Override + public void apply(Root root) throws RemoteCommitException { + for (ContentRemoteOperation operation : operations) { + operation.apply(root); + } + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/BasicContentRemoteCredentials.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/BasicContentRemoteCredentials.java new file mode 100644 index 0000000..ab6da4d --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/BasicContentRemoteCredentials.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.remote.RemoteLoginException; + +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.SimpleCredentials; +import javax.security.auth.login.LoginException; + +class BasicContentRemoteCredentials implements ContentRemoteCredentials { + + private final String user; + + private final char[] password; + + public BasicContentRemoteCredentials(String user, char[] password) { + this.user = user; + this.password = password; + } + + @Override + public ContentSession login(ContentRepository repository) throws RemoteLoginException { + ContentSession session; + + try { + session = repository.login(new SimpleCredentials(user, password), null); + } catch (LoginException e) { + throw new RemoteLoginException("unable to login", e); + } catch (NoSuchWorkspaceException e) { + throw new RemoteLoginException("unable to use the default workspace", e); + } + + return session; + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteBinaries.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteBinaries.java new file mode 100644 index 0000000..a163895 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteBinaries.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import com.google.common.collect.Maps; +import org.apache.jackrabbit.oak.api.Blob; + +import java.util.Map; +import java.util.UUID; + +class ContentRemoteBinaries { + + private Map binaries; + + public ContentRemoteBinaries() { + binaries = Maps.newHashMap(); + } + + public String put(Blob blob) { + String binaryId = UUID.randomUUID().toString(); + + binaries.put(binaryId, blob); + + return binaryId; + } + + public Blob get(String binaryId) { + return binaries.get(binaryId); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteBinaryId.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteBinaryId.java new file mode 100644 index 0000000..952c1a0 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteBinaryId.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.remote.RemoteBinaryId; + +class ContentRemoteBinaryId implements RemoteBinaryId { + + private final String reference; + + private final Blob blob; + + public ContentRemoteBinaryId(String reference, Blob blob) { + this.reference = reference; + this.blob = blob; + } + + public Blob asBlob() { + return blob; + } + + @Override + public String asString() { + return reference; + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteCredentials.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteCredentials.java new file mode 100644 index 0000000..f2a9303 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteCredentials.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.remote.RemoteCredentials; +import org.apache.jackrabbit.oak.remote.RemoteLoginException; + +interface ContentRemoteCredentials extends RemoteCredentials { + + ContentSession login(ContentRepository repository) throws RemoteLoginException; + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteInputStream.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteInputStream.java new file mode 100644 index 0000000..bcf667b --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteInputStream.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.remote.RemoteBinaryFilters; + +import java.io.IOException; +import java.io.InputStream; + +class ContentRemoteInputStream extends InputStream { + + private final InputStream stream; + + private final long start; + + private final long count; + + private long index = 0; + + public ContentRemoteInputStream(InputStream stream, RemoteBinaryFilters filters) { + this.stream = stream; + + long startFilter = filters.getStart(); + + if (startFilter > 0) { + this.start = startFilter; + } else { + this.start = 0; + } + + this.count = filters.getCount(); + } + + @Override + public int read() throws IOException { + while (index < start - 1) { + long skipped = stream.skip(start - index); + + if (skipped <= 0) { + return -1; + } + + index = index + skipped; + } + + if (count >= 0 && index >= start + count) { + return -1; + } + + int result = stream.read(); + + index = index + 1; + + return result; + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteOperation.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteOperation.java new file mode 100644 index 0000000..a608a11 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteOperation.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.remote.RemoteCommitException; +import org.apache.jackrabbit.oak.remote.RemoteOperation; + +interface ContentRemoteOperation extends RemoteOperation { + + void apply(Root root) throws RemoteCommitException; + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRepository.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRepository.java new file mode 100644 index 0000000..1b7abda --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRepository.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.remote.RemoteCredentials; +import org.apache.jackrabbit.oak.remote.RemoteLoginException; +import org.apache.jackrabbit.oak.remote.RemoteRepository; +import org.apache.jackrabbit.oak.remote.RemoteSession; + +import java.util.Set; + +public class ContentRemoteRepository implements RemoteRepository { + + private final ContentRepository contentRepository; + + private final ContentRemoteRevisions contentRemoteRevisions; + + private final ContentRemoteBinaries contentRemoteBinaries; + + public ContentRemoteRepository(ContentRepository contentRepository) { + this.contentRemoteRevisions = new ContentRemoteRevisions(); + this.contentRemoteBinaries = new ContentRemoteBinaries(); + this.contentRepository = contentRepository; + } + + @Override + public RemoteCredentials createBasicCredentials(final String user, final char[] password) { + return new BasicContentRemoteCredentials(user, password); + } + + @Override + public RemoteCredentials createImpersonationCredentials(Set principals) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public RemoteSession login(RemoteCredentials remoteCredentials) throws RemoteLoginException { + ContentRemoteCredentials contentRemoteCredentials = null; + + if (remoteCredentials instanceof ContentRemoteCredentials) { + contentRemoteCredentials = (ContentRemoteCredentials) remoteCredentials; + } + + if (contentRemoteCredentials == null) { + throw new IllegalArgumentException("invalid credentials"); + } + + Thread thread = Thread.currentThread(); + + ClassLoader loader = thread.getContextClassLoader(); + + thread.setContextClassLoader(Oak.class.getClassLoader()); + + ContentSession session; + + try { + session = contentRemoteCredentials.login(contentRepository); + } finally { + thread.setContextClassLoader(loader); + } + + return new ContentRemoteSession(session, contentRemoteRevisions, contentRemoteBinaries); + } + +} \ No newline at end of file diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResult.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResult.java new file mode 100644 index 0000000..d518f2c --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResult.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.ResultRow; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.remote.RemoteResult; +import org.apache.jackrabbit.oak.remote.RemoteValue; +import org.apache.jackrabbit.util.ISO8601; + +import javax.jcr.PropertyType; +import java.util.List; + +import static com.google.common.collect.Lists.newArrayList; + +class ContentRemoteResult implements RemoteResult { + + private final ContentRemoteBinaries binaries; + + private final ResultRow row; + + public ContentRemoteResult(ContentRemoteBinaries binaries, ResultRow row) { + this.binaries = binaries; + this.row = row; + } + + @Override + public RemoteValue getColumnValue(String column) { + return toRemoteValue(row.getValue(column)); + } + + private RemoteValue toRemoteValue(PropertyValue value) { + if (value == null) { + return null; + } + + Type type = value.getType(); + + if (type.isArray()) { + return toMultiRemoteValue(value); + } else { + return toSingleRemoteValue(value); + } + } + + private RemoteValue toSingleRemoteValue(PropertyValue value) { + Type type = value.getType(); + + switch (type.tag()) { + case PropertyType.STRING: + return RemoteValue.toText(value.getValue(Type.STRING)); + case PropertyType.BINARY: + return RemoteValue.toBinaryId(binaries.put(value.getValue(Type.BINARY))); + case PropertyType.LONG: + return RemoteValue.toLong(value.getValue(Type.LONG)); + case PropertyType.DOUBLE: + return RemoteValue.toDouble(value.getValue(Type.DOUBLE)); + case PropertyType.DATE: + return RemoteValue.toDate(ISO8601.parse(value.getValue(Type.DATE)).getTimeInMillis()); + case PropertyType.BOOLEAN: + return RemoteValue.toBoolean(value.getValue(Type.BOOLEAN)); + case PropertyType.NAME: + return RemoteValue.toName(value.getValue(Type.NAME)); + case PropertyType.PATH: + return RemoteValue.toPath(value.getValue(Type.PATH)); + case PropertyType.REFERENCE: + return RemoteValue.toReference(value.getValue(Type.REFERENCE)); + case PropertyType.WEAKREFERENCE: + return RemoteValue.toWeakReference(value.getValue(Type.WEAKREFERENCE)); + case PropertyType.URI: + return RemoteValue.toUri(value.getValue(Type.URI)); + case PropertyType.DECIMAL: + return RemoteValue.toDecimal(value.getValue(Type.DECIMAL)); + } + + throw new IllegalStateException("type not supported"); + } + + private RemoteValue toMultiRemoteValue(PropertyValue value) { + Type type = value.getType(); + + switch (type.tag()) { + case PropertyType.STRING: + return RemoteValue.toMultiText(value.getValue(Type.STRINGS)); + case PropertyType.BINARY: + return RemoteValue.toMultiBinaryId(readBinaryValues(value)); + case PropertyType.LONG: + return RemoteValue.toMultiLong(value.getValue(Type.LONGS)); + case PropertyType.DOUBLE: + return RemoteValue.toMultiDouble(value.getValue(Type.DOUBLES)); + case PropertyType.DATE: + return RemoteValue.toMultiDate(readDateValues(value)); + case PropertyType.BOOLEAN: + return RemoteValue.toMultiBoolean(value.getValue(Type.BOOLEANS)); + case PropertyType.NAME: + return RemoteValue.toMultiName(value.getValue(Type.NAMES)); + case PropertyType.PATH: + return RemoteValue.toMultiPath(value.getValue(Type.PATHS)); + case PropertyType.REFERENCE: + return RemoteValue.toMultiReference(value.getValue(Type.REFERENCES)); + case PropertyType.WEAKREFERENCE: + return RemoteValue.toMultiWeakReference(value.getValue(Type.WEAKREFERENCES)); + case PropertyType.URI: + return RemoteValue.toMultiUri(value.getValue(Type.URIS)); + case PropertyType.DECIMAL: + return RemoteValue.toMultiDecimal(value.getValue(Type.DECIMALS)); + } + + throw new IllegalStateException("type not supported"); + } + + private Iterable readBinaryValues(PropertyValue value) { + List result = newArrayList(); + + for (Blob blob : value.getValue(Type.BINARIES)) { + result.add(binaries.put(blob)); + } + + return result; + } + + private Iterable readDateValues(PropertyValue value) { + List result = newArrayList(); + + for (String string : value.getValue(Type.DATES)) { + result.add(ISO8601.parse(string).getTimeInMillis()); + } + + return result; + } + + @Override + public String getSelectorPath(String selector) { + return row.getPath(selector); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResults.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResults.java new file mode 100644 index 0000000..ae1da11 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResults.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.Result; +import org.apache.jackrabbit.oak.api.ResultRow; +import org.apache.jackrabbit.oak.remote.RemoteResult; +import org.apache.jackrabbit.oak.remote.RemoteResults; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import static com.google.common.collect.Lists.newArrayList; + +class ContentRemoteResults implements RemoteResults { + + private final ContentRemoteBinaries binaries; + + private final Result results; + + public ContentRemoteResults(ContentRemoteBinaries binaries, Result results) { + this.binaries = binaries; + this.results = results; + } + + @Override + public long getTotal() { + return results.getSize(); + } + + @Override + public Iterable getColumns() { + return Arrays.asList(results.getColumnNames()); + } + + @Override + public Iterable getSelectors() { + return Arrays.asList(results.getSelectorNames()); + } + + @Override + public Iterator iterator() { + return getResults().iterator(); + } + + private Iterable getResults() { + List results = newArrayList(); + + for (ResultRow row : this.results.getRows()) { + results.add(new ContentRemoteResult(binaries, row)); + } + + return results; + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRevision.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRevision.java new file mode 100644 index 0000000..483e71e --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRevision.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.remote.RemoteRevision; + +class ContentRemoteRevision implements RemoteRevision { + + private final String id; + + private final Root root; + + public ContentRemoteRevision(String id, Root root) { + this.id = id; + this.root = root; + } + + @Override + public String asString() { + return id; + } + + public Root getRoot() { + return root; + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRevisions.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRevisions.java new file mode 100644 index 0000000..8d9ac0e --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRevisions.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import com.google.common.base.Objects; +import com.google.common.collect.Maps; +import org.apache.jackrabbit.oak.api.AuthInfo; +import org.apache.jackrabbit.oak.api.Root; + +import java.security.Principal; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +class ContentRemoteRevisions { + + private class Key { + + private final String revisionId; + + private final Set principals; + + private final String user; + + private Key(AuthInfo authInfo, String revisionId) { + this.user = authInfo.getUserID(); + this.principals = authInfo.getPrincipals(); + this.revisionId = revisionId; + } + + @Override + public boolean equals(Object object) { + if (object == null) { + return false; + } + + if (object == this) { + return true; + } + + if (getClass() != object.getClass()) { + return false; + } + + Key other = (Key) object; + + if (!Objects.equal(revisionId, other.revisionId)) { + return false; + } + + if (!Objects.equal(user, other.user)) { + return false; + } + + if (!Objects.equal(principals, other.principals)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return Objects.hashCode(revisionId, user, principals); + } + + } + + private Map roots; + + public ContentRemoteRevisions() { + this.roots = Maps.newHashMap(); + } + + private Key key(AuthInfo authInfo, String revisionId) { + return new Key(authInfo, revisionId); + } + + public Root get(AuthInfo authInfo, String revisionId) { + return roots.get(key(authInfo, revisionId)); + } + + public String put(AuthInfo authInfo, Root root) { + String revisionId = UUID.randomUUID().toString(); + + roots.put(key(authInfo, revisionId), root); + + return revisionId; + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteSession.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteSession.java new file mode 100644 index 0000000..15f9cc5 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteSession.java @@ -0,0 +1,454 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Result; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.remote.RemoteBinaryFilters; +import org.apache.jackrabbit.oak.remote.RemoteBinaryId; +import org.apache.jackrabbit.oak.remote.RemoteCommitException; +import org.apache.jackrabbit.oak.remote.RemoteOperation; +import org.apache.jackrabbit.oak.remote.RemoteQueryParseException; +import org.apache.jackrabbit.oak.remote.RemoteResults; +import org.apache.jackrabbit.oak.remote.RemoteRevision; +import org.apache.jackrabbit.oak.remote.RemoteSession; +import org.apache.jackrabbit.oak.remote.RemoteTreeFilters; +import org.apache.jackrabbit.oak.remote.RemoteValue; + +import java.io.IOException; +import java.io.InputStream; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.apache.jackrabbit.oak.commons.PathUtils.denotesRoot; +import static org.apache.jackrabbit.oak.commons.PathUtils.isAbsolute; +import static org.apache.jackrabbit.oak.commons.PathUtils.isAncestor; + +class ContentRemoteSession implements RemoteSession { + + private final ContentSession contentSession; + + private final ContentRemoteRevisions contentRemoteRevisions; + + private final ContentRemoteBinaries contentRemoteBinaries; + + public ContentRemoteSession(ContentSession contentSession, ContentRemoteRevisions contentRemoteRevisions, ContentRemoteBinaries contentRemoteBinaries) { + this.contentSession = contentSession; + this.contentRemoteRevisions = contentRemoteRevisions; + this.contentRemoteBinaries = contentRemoteBinaries; + } + + @Override + public ContentRemoteRevision readLastRevision() { + Root root = contentSession.getLatestRoot(); + String revisionId = contentRemoteRevisions.put(contentSession.getAuthInfo(), root); + return new ContentRemoteRevision(revisionId, root); + } + + @Override + public ContentRemoteRevision readRevision(String revisionId) { + Root root = contentRemoteRevisions.get(contentSession.getAuthInfo(), revisionId); + + if (root == null) { + return null; + } + + return new ContentRemoteRevision(revisionId, root); + } + + @Override + public ContentRemoteTree readTree(RemoteRevision revision, String path, RemoteTreeFilters filters) { + ContentRemoteRevision contentRemoteRevision = null; + + if (revision instanceof ContentRemoteRevision) { + contentRemoteRevision = (ContentRemoteRevision) revision; + } + + if (contentRemoteRevision == null) { + throw new IllegalArgumentException("revision not provided"); + } + + if (path == null) { + throw new IllegalArgumentException("path not provided"); + } + + if (!isAbsolute(path)) { + throw new IllegalArgumentException("invalid path"); + } + + if (filters == null) { + throw new IllegalArgumentException("filters not provided"); + } + + Root root = contentRemoteRevision.getRoot(); + + if (root == null) { + throw new IllegalStateException("unable to locate the root"); + } + + Tree tree = root.getTree(path); + + if (tree.exists()) { + return new ContentRemoteTree(tree, 0, filters, contentRemoteBinaries); + } + + return null; + } + + @Override + public ContentRemoteOperation createAddOperation(String path, Map properties) { + if (path == null) { + throw new IllegalArgumentException("path not provided"); + } + + if (!isAbsolute(path)) { + throw new IllegalArgumentException("invalid path"); + } + + if (denotesRoot(path)) { + throw new IllegalArgumentException("adding root node"); + } + + if (properties == null) { + throw new IllegalArgumentException("properties not provided"); + } + + List operations = new ArrayList(); + + operations.add(new AddContentRemoteOperation(path)); + + for (Map.Entry entry : properties.entrySet()) { + operations.add(createSetOperation(path, entry.getKey(), entry.getValue())); + } + + return new AggregateContentRemoteOperation(operations); + } + + @Override + public ContentRemoteOperation createRemoveOperation(String path) { + if (path == null) { + throw new IllegalArgumentException("path not provided"); + } + + if (!isAbsolute(path)) { + throw new IllegalArgumentException("invalid path"); + } + + if (denotesRoot(path)) { + throw new IllegalArgumentException("removing root node"); + } + + return new RemoveContentRemoteOperation(path); + } + + @Override + public ContentRemoteOperation createSetOperation(String path, String name, RemoteValue value) { + if (path == null) { + throw new IllegalArgumentException("path not provided"); + } + + if (!isAbsolute(path)) { + throw new IllegalArgumentException("invalid path"); + } + + if (name == null) { + throw new IllegalArgumentException("name not provided"); + } + + if (name.isEmpty()) { + throw new IllegalArgumentException("name is empty"); + } + + if (value == null) { + throw new IllegalArgumentException("value not provided"); + } + + return new SetContentRemoteOperation(contentRemoteBinaries, path, name, value); + } + + @Override + public ContentRemoteOperation createUnsetOperation(String path, String name) { + if (path == null) { + throw new IllegalArgumentException("path not provided"); + } + + if (!isAbsolute(path)) { + throw new IllegalArgumentException("invalid path"); + } + + if (name == null) { + throw new IllegalArgumentException("name not provided"); + } + + if (name.isEmpty()) { + throw new IllegalArgumentException("name is empty"); + } + + return new UnsetContentRemoteOperation(path, name); + } + + @Override + public ContentRemoteOperation createCopyOperation(String source, String target) { + if (source == null) { + throw new IllegalArgumentException("source path not provided"); + } + + if (!isAbsolute(source)) { + throw new IllegalArgumentException("invalid source path"); + } + + if (target == null) { + throw new IllegalArgumentException("target path not provided"); + } + + if (!isAbsolute(target)) { + throw new IllegalArgumentException("invalid target path"); + } + + if (source.equals(target)) { + throw new IllegalArgumentException("same source and target path"); + } + + if (isAncestor(source, target)) { + throw new IllegalArgumentException("source path is an ancestor of target path"); + } + + return new CopyContentRemoteOperation(source, target); + } + + @Override + public ContentRemoteOperation createMoveOperation(String source, String target) { + if (source == null) { + throw new IllegalArgumentException("source path not provided"); + } + + if (!isAbsolute(source)) { + throw new IllegalArgumentException("invalid source path"); + } + + if (target == null) { + throw new IllegalArgumentException("target path not provided"); + } + + if (!isAbsolute(target)) { + throw new IllegalArgumentException("invalid target path"); + } + + if (source.equals(target)) { + throw new IllegalArgumentException("same source and target path"); + } + + if (isAncestor(source, target)) { + throw new IllegalArgumentException("source path is an ancestor of target path"); + } + + return new MoveContentRemoteOperation(source, target); + } + + @Override + public ContentRemoteOperation createAggregateOperation(final List operations) { + if (operations == null) { + throw new IllegalArgumentException("operations not provided"); + } + + List contentRemoteOperations = new ArrayList(); + + for (RemoteOperation operation : operations) { + if (operation == null) { + throw new IllegalArgumentException("operation not provided"); + } + + ContentRemoteOperation contentRemoteOperation = null; + + if (operation instanceof ContentRemoteOperation) { + contentRemoteOperation = (ContentRemoteOperation) operation; + } + + if (contentRemoteOperation == null) { + throw new IllegalArgumentException("invalid operation"); + } + + contentRemoteOperations.add(contentRemoteOperation); + } + + return new AggregateContentRemoteOperation(contentRemoteOperations); + } + + @Override + public ContentRemoteRevision commit(RemoteRevision revision, RemoteOperation operation) throws RemoteCommitException { + ContentRemoteRevision contentRemoteRevision = null; + + if (revision instanceof ContentRemoteRevision) { + contentRemoteRevision = (ContentRemoteRevision) revision; + } + + if (contentRemoteRevision == null) { + throw new IllegalArgumentException("invalid revision"); + } + + ContentRemoteOperation contentRemoteOperation = null; + + if (operation instanceof ContentRemoteOperation) { + contentRemoteOperation = (ContentRemoteOperation) operation; + } + + if (contentRemoteOperation == null) { + throw new IllegalArgumentException("invalid operation"); + } + + Root root = contentRemoteRevision.getRoot(); + + if (root == null) { + throw new IllegalStateException("unable to locate the root"); + } + + contentRemoteOperation.apply(root); + + try { + root.commit(); + } catch (CommitFailedException e) { + throw new RemoteCommitException("unable to apply the changes", e); + } + + return new ContentRemoteRevision(contentRemoteRevisions.put(contentSession.getAuthInfo(), root), root); + } + + @Override + public ContentRemoteBinaryId readBinaryId(String binaryId) { + if (binaryId == null) { + throw new IllegalArgumentException("binary id not provided"); + } + + if (binaryId.isEmpty()) { + throw new IllegalArgumentException("invalid binary id"); + } + + Blob blob = contentRemoteBinaries.get(binaryId); + + if (blob == null) { + return null; + } + + return new ContentRemoteBinaryId(binaryId, blob); + } + + @Override + public InputStream readBinary(RemoteBinaryId binaryId, RemoteBinaryFilters filters) { + ContentRemoteBinaryId contentRemoteBinaryId = null; + + if (binaryId instanceof ContentRemoteBinaryId) { + contentRemoteBinaryId = (ContentRemoteBinaryId) binaryId; + } + + if (contentRemoteBinaryId == null) { + throw new IllegalArgumentException("invalid binary id"); + } + + if (filters == null) { + throw new IllegalArgumentException("filters not provided"); + } + + return new ContentRemoteInputStream(contentRemoteBinaryId.asBlob().getNewStream(), filters); + } + + @Override + public long readBinaryLength(RemoteBinaryId binaryId) { + ContentRemoteBinaryId contentRemoteBinaryId = null; + + if (binaryId instanceof ContentRemoteBinaryId) { + contentRemoteBinaryId = (ContentRemoteBinaryId) binaryId; + } + + if (contentRemoteBinaryId == null) { + throw new IllegalArgumentException("invalid binary id"); + } + + return contentRemoteBinaryId.asBlob().length(); + } + + @Override + public ContentRemoteBinaryId writeBinary(InputStream stream) { + if (stream == null) { + throw new IllegalArgumentException("stream not provided"); + } + + Blob blob; + + try { + blob = contentSession.getLatestRoot().createBlob(stream); + } catch (IOException e) { + throw new RuntimeException("unable to write the binary object", e); + } + + return new ContentRemoteBinaryId(contentRemoteBinaries.put(blob), blob); + } + + @Override + public RemoteResults search(RemoteRevision revision, String query, String language, long offset, long limit) throws RemoteQueryParseException { + ContentRemoteRevision contentRemoteRevision = null; + + if (revision instanceof ContentRemoteRevision) { + contentRemoteRevision = (ContentRemoteRevision) revision; + } + + if (contentRemoteRevision == null) { + throw new IllegalArgumentException("invalid revision"); + } + + Root root = contentRemoteRevision.getRoot(); + + if (query == null) { + throw new IllegalArgumentException("query not provided"); + } + + if (language == null) { + throw new IllegalArgumentException("language not provided"); + } + + if (!root.getQueryEngine().getSupportedQueryLanguages().contains(language)) { + throw new IllegalArgumentException("language not supported"); + } + + if (offset < 0) { + throw new IllegalArgumentException("invalid offset"); + } + + if (limit < 0) { + throw new IllegalArgumentException("invalid limit"); + } + + Result results; + + try { + results = root.getQueryEngine().executeQuery(query, language, limit, offset, new HashMap(), new HashMap()); + } catch (ParseException e) { + throw new RemoteQueryParseException("invalid query", e); + } + + return new ContentRemoteResults(contentRemoteBinaries, results); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteTree.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteTree.java new file mode 100644 index 0000000..427a536 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteTree.java @@ -0,0 +1,335 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.remote.RemoteTree; +import org.apache.jackrabbit.oak.remote.RemoteTreeFilters; +import org.apache.jackrabbit.oak.remote.RemoteValue; +import org.apache.jackrabbit.oak.remote.RemoteValue.Supplier; +import org.apache.jackrabbit.oak.remote.filter.Filters; +import org.apache.jackrabbit.util.ISO8601; + +import java.io.InputStream; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; + +class ContentRemoteTree implements RemoteTree { + + private final Tree tree; + + private final int depth; + + private final RemoteTreeFilters filters; + + private final ContentRemoteBinaries contentRemoteBinaries; + + public ContentRemoteTree(Tree tree, int depth, RemoteTreeFilters filters, ContentRemoteBinaries contentRemoteBinaries) { + this.tree = tree; + this.depth = depth; + this.filters = filters; + this.contentRemoteBinaries = contentRemoteBinaries; + } + + @Override + public Map getProperties() { + Map properties = new HashMap(); + + for (PropertyState property : getFilteredProperties()) { + properties.put(property.getName(), getRemoteValue(property)); + } + + return properties; + } + + private Iterable getFilteredProperties() { + return Iterables.filter(tree.getProperties(), getPropertyFilters()); + } + + private Predicate getPropertyFilters() { + return new Predicate() { + + @Override + public boolean apply(PropertyState property) { + return new Filters(filters.getPropertyFilters()).matches(property.getName()); + } + + }; + } + + private RemoteValue getRemoteValue(PropertyState property) { + Type type = property.getType(); + + if (type == Type.DATE) { + return RemoteValue.toDate(getDate(property.getValue(Type.DATE))); + } + + if (type == Type.DATES) { + return RemoteValue.toMultiDate(getDates(property.getValue(Type.DATES))); + } + + if (type == Type.BINARY) { + return getBinaryRemoteValue(property.getValue(Type.BINARY)); + } + + if (type == Type.BINARIES) { + return getBinaryRemoteValues(property.getValue(Type.BINARIES)); + } + + if (type == Type.BOOLEAN) { + return RemoteValue.toBoolean(property.getValue(Type.BOOLEAN)); + } + + if (type == Type.BOOLEANS) { + return RemoteValue.toMultiBoolean(property.getValue(Type.BOOLEANS)); + } + + if (type == Type.DECIMAL) { + return RemoteValue.toDecimal(property.getValue(Type.DECIMAL)); + } + + if (type == Type.DECIMALS) { + return RemoteValue.toMultiDecimal(property.getValue(Type.DECIMALS)); + } + + if (type == Type.DOUBLE) { + return RemoteValue.toDouble(property.getValue(Type.DOUBLE)); + } + + if (type == Type.DOUBLES) { + return RemoteValue.toMultiDouble(property.getValue(Type.DOUBLES)); + } + + if (type == Type.LONG) { + return RemoteValue.toLong(property.getValue(Type.LONG)); + } + + if (type == Type.LONGS) { + return RemoteValue.toMultiLong(property.getValue(Type.LONGS)); + } + + if (type == Type.NAME) { + return RemoteValue.toName(property.getValue(Type.NAME)); + } + + if (type == Type.NAMES) { + return RemoteValue.toMultiName(property.getValue(Type.NAMES)); + } + + if (type == Type.PATH) { + return RemoteValue.toPath(property.getValue(Type.PATH)); + } + + if (type == Type.PATHS) { + return RemoteValue.toMultiPath(property.getValue(Type.PATHS)); + } + + if (type == Type.REFERENCE) { + return RemoteValue.toReference(property.getValue(Type.REFERENCE)); + } + + if (type == Type.REFERENCES) { + return RemoteValue.toMultiReference(property.getValue(Type.REFERENCES)); + } + + if (type == Type.STRING) { + return RemoteValue.toText(property.getValue(Type.STRING)); + } + + if (type == Type.STRINGS) { + return RemoteValue.toMultiText(property.getValue(Type.STRINGS)); + } + + if (type == Type.URI) { + return RemoteValue.toUri(property.getValue(Type.URI)); + } + + if (type == Type.URIS) { + return RemoteValue.toMultiUri(property.getValue(Type.URIS)); + } + + if (type == Type.WEAKREFERENCE) { + return RemoteValue.toWeakReference(property.getValue(Type.WEAKREFERENCE)); + } + + if (type == Type.WEAKREFERENCES) { + return RemoteValue.toMultiWeakReference(property.getValue(Type.WEAKREFERENCES)); + } + + throw new IllegalArgumentException("unrecognized property type"); + } + + private long getDate(String date) { + Calendar calendar = ISO8601.parse(date); + + if (calendar == null) { + throw new IllegalStateException("invalid date format"); + } + + return calendar.getTimeInMillis(); + } + + private Iterable getDates(Iterable dates) { + return Iterables.transform(dates, new Function() { + + @Override + public Long apply(String date) { + return getDate(date); + } + + }); + } + + private RemoteValue getBinaryRemoteValue(Blob blob) { + if (getLength(blob) < filters.getBinaryThreshold()) { + return RemoteValue.toBinary(getBinary(blob)); + } else { + return RemoteValue.toBinaryId(getBinaryId(blob)); + } + } + + private RemoteValue getBinaryRemoteValues(Iterable blobs) { + if (getLength(blobs) < filters.getBinaryThreshold()) { + return RemoteValue.toMultiBinary(getBinaries(blobs)); + } else { + return RemoteValue.toMultiBinaryId(getBinaryIds(blobs)); + } + } + + private long getLength(Blob blob) { + return blob.length(); + } + + private long getLength(Iterable blobs) { + long length = 0; + + for (Blob blob : blobs) { + length = length + blob.length(); + } + + return length; + } + + private Supplier getBinary(final Blob blob) { + return new Supplier() { + + @Override + public InputStream get() { + return blob.getNewStream(); + } + + }; + } + + private Iterable> getBinaries(Iterable blobs) { + return Iterables.transform(blobs, new Function>() { + + @Override + public Supplier apply(Blob blob) { + return getBinary(blob); + } + + }); + } + + private String getBinaryId(Blob blob) { + return contentRemoteBinaries.put(blob); + } + + private Iterable getBinaryIds(Iterable blobs) { + return Iterables.transform(blobs, new Function() { + + @Override + public String apply(Blob blob) { + return getBinaryId(blob); + } + + }); + } + + @Override + public Map getChildren() { + Map children = new HashMap(); + + for (Tree child : getFilteredChildren()) { + if (depth < filters.getDepth()) { + children.put(child.getName(), new ContentRemoteTree(child, depth + 1, filters, contentRemoteBinaries)); + } else { + children.put(child.getName(), null); + } + } + + return children; + } + + private Iterable getFilteredChildren() { + Iterable result = tree.getChildren(); + + if (filters.getChildrenStart() > 0) { + result = Iterables.skip(result, filters.getChildrenStart()); + } + + if (filters.getChildrenCount() >= 0) { + result = Iterables.limit(result, filters.getChildrenCount()); + } + + return Iterables.filter(result, getNodeFilters()); + } + + private Predicate getNodeFilters() { + return new Predicate() { + + @Override + public boolean apply(Tree child) { + return new Filters(filters.getNodeFilters()).matches(child.getName()); + } + + }; + } + + @Override + public boolean hasMoreChildren() { + if (filters.getChildrenCount() < 0) { + return false; + } + + int start = filters.getChildrenStart(); + + if (start < 0) { + start = 0; + } + + int count = filters.getChildrenCount(); + + if (count < 0) { + count = 0; + } + + int max = start + count; + + return tree.getChildrenCount(max) > max; + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/CopyContentRemoteOperation.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/CopyContentRemoteOperation.java new file mode 100644 index 0000000..ed23b06 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/CopyContentRemoteOperation.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.remote.RemoteCommitException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class CopyContentRemoteOperation implements ContentRemoteOperation { + + private static final Logger logger = LoggerFactory.getLogger(CopyContentRemoteOperation.class); + + private final String source; + + private final String target; + + public CopyContentRemoteOperation(String source, String target) { + this.source = source; + this.target = target; + } + + @Override + public void apply(Root root) throws RemoteCommitException { + logger.debug("performing 'copy' operation on source={}, target={}", source, target); + + Tree sourceTree = root.getTree(source); + + if (!sourceTree.exists()) { + throw new RemoteCommitException("source tree does not exist"); + } + + Tree targetTree = root.getTree(target); + + if (targetTree.exists()) { + throw new RemoteCommitException("target tree already exists"); + } + + Tree targetParentTree = targetTree.getParent(); + + if (!targetParentTree.exists()) { + throw new RemoteCommitException("parent of target tree does not exist"); + } + + copy(sourceTree, targetParentTree, targetTree.getName()); + } + + private void copy(Tree source, Tree targetParent, String targetName) { + Tree target = targetParent.addChild(targetName); + + for (PropertyState property : source.getProperties()) { + target.setProperty(property); + } + + for (Tree child : source.getChildren()) { + copy(child, target, child.getName()); + } + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/MoveContentRemoteOperation.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/MoveContentRemoteOperation.java new file mode 100644 index 0000000..9199d5e --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/MoveContentRemoteOperation.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.remote.RemoteCommitException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class MoveContentRemoteOperation implements ContentRemoteOperation { + + private static final Logger logger = LoggerFactory.getLogger(MoveContentRemoteOperation.class); + + private final String source; + + private final String target; + + public MoveContentRemoteOperation(String source, String target) { + this.source = source; + this.target = target; + } + + @Override + public void apply(Root root) throws RemoteCommitException { + logger.debug("performing 'move' operation on source={}, target={}", source, target); + + boolean success = root.move(source, target); + + if (success) { + return; + } + + throw new RemoteCommitException("unable to move the tree"); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/RemoveContentRemoteOperation.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/RemoveContentRemoteOperation.java new file mode 100644 index 0000000..177a285 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/RemoveContentRemoteOperation.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.remote.RemoteCommitException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class RemoveContentRemoteOperation implements ContentRemoteOperation { + + private static final Logger logger = LoggerFactory.getLogger(RemoveContentRemoteOperation.class); + + private final String path; + + public RemoveContentRemoteOperation(String path) { + this.path = path; + } + + @Override + public void apply(Root root) throws RemoteCommitException { + logger.debug("performing 'remove' operation on path={}", path); + + Tree tree = root.getTree(path); + + if (!tree.exists()) { + throw new RemoteCommitException("tree does not exists"); + } + + if (!tree.remove()) { + throw new RemoteCommitException("unable to remove the tree"); + } + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetContentRemoteOperation.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetContentRemoteOperation.java new file mode 100644 index 0000000..c805304 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetContentRemoteOperation.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.remote.RemoteCommitException; +import org.apache.jackrabbit.oak.remote.RemoteValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class SetContentRemoteOperation implements ContentRemoteOperation { + + private static final Logger logger = LoggerFactory.getLogger(SetContentRemoteOperation.class); + + private final ContentRemoteBinaries binaries; + + private final String path; + + private final String name; + + private final RemoteValue value; + + public SetContentRemoteOperation(ContentRemoteBinaries binaries, String path, String name, RemoteValue value) { + this.binaries = binaries; + this.path = path; + this.name = name; + this.value = value; + } + + @Override + public void apply(Root root) throws RemoteCommitException { + logger.debug("performing 'set' operation on path={}, name={}", path, name); + + Tree tree = root.getTree(path); + + if (!tree.exists()) { + throw new RemoteCommitException("tree does not exist"); + } + + value.whenType(new SetPropertyHandler(binaries, root, tree, name)); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetPropertyHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetPropertyHandler.java new file mode 100644 index 0000000..5b4a323 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/SetPropertyHandler.java @@ -0,0 +1,242 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import com.google.common.base.Function; +import com.google.common.collect.Iterables; +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.remote.RemoteValue.Supplier; +import org.apache.jackrabbit.oak.remote.RemoteValue.TypeHandler; +import org.apache.jackrabbit.util.ISO8601; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.Calendar; + +class SetPropertyHandler extends TypeHandler { + + private final ContentRemoteBinaries binaries; + + private final Root root; + + private final Tree tree; + + private final String name; + + public SetPropertyHandler(ContentRemoteBinaries binaries, Root root, Tree tree, String name) { + this.binaries = binaries; + this.root = root; + this.tree = tree; + this.name = name; + } + + @Override + public void isBinary(Supplier value) { + tree.setProperty(name, getBlob(root, value), Type.BINARY); + } + + @Override + public void isMultiBinary(Iterable> value) { + tree.setProperty(name, getBlobs(root, value), Type.BINARIES); + } + + @Override + public void isBinaryId(String value) { + tree.setProperty(name, getBlobFromId(binaries, value), Type.BINARY); + } + + @Override + public void isMultiBinaryId(Iterable value) { + tree.setProperty(name, getBlobsFromIds(binaries, value), Type.BINARIES); + } + + @Override + public void isBoolean(Boolean value) { + tree.setProperty(name, value, Type.BOOLEAN); + } + + @Override + public void isMultiBoolean(Iterable value) { + tree.setProperty(name, value, Type.BOOLEANS); + } + + @Override + public void isDate(Long value) { + tree.setProperty(name, getDate(value), Type.DATE); + } + + @Override + public void isMultiDate(Iterable value) { + tree.setProperty(name, getDates(value), Type.DATES); + } + + @Override + public void isDecimal(BigDecimal value) { + tree.setProperty(name, value, Type.DECIMAL); + } + + @Override + public void isMultiDecimal(Iterable value) { + tree.setProperty(name, value, Type.DECIMALS); + } + + @Override + public void isDouble(Double value) { + tree.setProperty(name, value, Type.DOUBLE); + } + + @Override + public void isMultiDouble(Iterable value) { + tree.setProperty(name, value, Type.DOUBLES); + } + + @Override + public void isLong(Long value) { + tree.setProperty(name, value, Type.LONG); + } + + @Override + public void isMultiLong(Iterable value) { + tree.setProperty(name, value, Type.LONGS); + } + + @Override + public void isName(String value) { + tree.setProperty(name, value, Type.NAME); + } + + @Override + public void isMultiName(Iterable value) { + tree.setProperty(name, value, Type.NAMES); + } + + @Override + public void isPath(String value) { + tree.setProperty(name, value, Type.PATH); + } + + @Override + public void isMultiPath(Iterable value) { + tree.setProperty(name, value, Type.PATHS); + } + + @Override + public void isReference(String value) { + tree.setProperty(name, value, Type.REFERENCE); + } + + @Override + public void isMultiReference(Iterable value) { + tree.setProperty(name, value, Type.REFERENCES); + } + + @Override + public void isText(String value) { + tree.setProperty(name, value, Type.STRING); + } + + @Override + public void isMultiText(Iterable value) { + tree.setProperty(name, value, Type.STRINGS); + } + + @Override + public void isUri(String value) { + tree.setProperty(name, value, Type.URI); + } + + @Override + public void isMultiUri(Iterable value) { + tree.setProperty(name, value, Type.URIS); + } + + @Override + public void isWeakReference(String value) { + tree.setProperty(name, value, Type.WEAKREFERENCE); + } + + @Override + public void isMultiWeakReference(Iterable value) { + tree.setProperty(name, value, Type.WEAKREFERENCES); + } + + private Blob getBlob(Root root, Supplier supplier) { + InputStream inputStream = supplier.get(); + + if (inputStream == null) { + throw new IllegalStateException("invalid input stream"); + } + + Blob blob; + + try { + blob = root.createBlob(inputStream); + } catch (Exception e) { + throw new IllegalStateException("unable to create a blob", e); + } + + return blob; + } + + private Iterable getBlobs(final Root root, Iterable> suppliers) { + return Iterables.transform(suppliers, new Function, Blob>() { + + @Override + public Blob apply(Supplier supplier) { + return getBlob(root, supplier); + } + + }); + } + + private Blob getBlobFromId(ContentRemoteBinaries binaries, String binaryId) { + return binaries.get(binaryId); + } + + private Iterable getBlobsFromIds(final ContentRemoteBinaries binaries, Iterable binaryIds) { + return Iterables.transform(binaryIds, new Function() { + + @Override + public Blob apply(String binaryId) { + return getBlobFromId(binaries, binaryId); + } + + }); + } + + private String getDate(Long time) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(time); + return ISO8601.format(calendar); + } + + private Iterable getDates(Iterable times) { + return Iterables.transform(times, new Function() { + + @Override + public String apply(Long time) { + return getDate(time); + } + + }); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/UnsetContentRemoteOperation.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/UnsetContentRemoteOperation.java new file mode 100644 index 0000000..c60aa92 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/content/UnsetContentRemoteOperation.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.remote.RemoteCommitException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class UnsetContentRemoteOperation implements ContentRemoteOperation { + + private static final Logger logger = LoggerFactory.getLogger(UnsetContentRemoteOperation.class); + + private final String path; + + private final String name; + + public UnsetContentRemoteOperation(String path, String name) { + this.path = path; + this.name = name; + } + + @Override + public void apply(Root root) throws RemoteCommitException { + logger.debug("performing 'unset' operation on path={}, name={}", path, name); + + Tree tree = root.getTree(path); + + if (!tree.exists()) { + throw new RemoteCommitException("tree does not exists"); + } + + if (!tree.hasProperty(name)) { + throw new RemoteCommitException("property does not exist"); + } + + tree.removeProperty(name); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filter.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filter.java new file mode 100644 index 0000000..1b37681 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filter.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.filter; + +import java.util.regex.Pattern; + +class Filter { + + private Pattern pattern; + + public Filter(String filter) { + StringBuilder builder = new StringBuilder(); + + int star = filter.indexOf('*'); + + while (star != -1) { + if (star > 0 && filter.charAt(star - 1) == '\\') { + builder.append(Pattern.quote(filter.substring(0, star - 1))); + builder.append(Pattern.quote("*")); + } else { + builder.append(Pattern.quote(filter.substring(0, star))); + builder.append(".*"); + } + filter = filter.substring(star + 1); + star = filter.indexOf('*'); + } + + builder.append(Pattern.quote(filter)); + + pattern = Pattern.compile(builder.toString()); + } + + public boolean matches(String name) { + return pattern.matcher(name).matches(); + } + +} \ No newline at end of file diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filters.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filters.java new file mode 100644 index 0000000..baf77a5 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/filter/Filters.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.filter; + +import java.util.HashSet; +import java.util.Set; + +public class Filters { + + private Set includes = new HashSet(); + + private Set excludes = new HashSet(); + + public Filters(Set filters) { + if (filters == null) { + throw new IllegalArgumentException("filter set is null"); + } + + for (String filter : filters) { + if (filter == null) { + throw new IllegalArgumentException("filter is null"); + } + + if (filter.length() == 0) { + throw new IllegalArgumentException("include filter is an empty string"); + } + + if (filter.startsWith("-") && filter.length() == 1) { + throw new IllegalArgumentException("exclude filter is an empty string"); + } + + } + + for (String filter : filters) { + if (filter.startsWith("-")) { + excludes.add(new Filter(filter.substring(1))); + } else { + includes.add(new Filter(filter)); + } + } + + if (includes.isEmpty()) { + includes.add(new Filter("*")); + } + } + + public boolean matches(String name) { + for (Filter include : includes) { + if (include.matches(name)) { + for (Filter exclude : excludes) { + if (exclude.matches(name)) { + return false; + } + } + + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteHandler.java new file mode 100644 index 0000000..d9a11fe --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteHandler.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http; + +import org.apache.jackrabbit.oak.remote.http.handler.Handler; +import org.apache.jackrabbit.oak.remote.http.matcher.Matcher; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +class RemoteHandler implements Matcher, Handler { + + private Matcher matcher; + + private Handler handler; + + public RemoteHandler(Matcher matcher, Handler handler) { + this.matcher = matcher; + this.handler = handler; + } + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + handler.handle(request, response); + } + + @Override + public boolean match(HttpServletRequest request) { + return matcher.match(request); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteServlet.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteServlet.java new file mode 100644 index 0000000..1e0e77f --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/RemoteServlet.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http; + +import org.apache.jackrabbit.oak.remote.RemoteRepository; +import org.apache.jackrabbit.oak.remote.http.handler.Handler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; + +import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createGetBinaryHandler; +import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createGetLastRevisionHandler; +import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createGetLastTreeHandler; +import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createGetRevisionTreeHandler; +import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createHeadBinaryHandler; +import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createHeadLastTreeHandler; +import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createHeadRevisionTreeHandler; +import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createNotFoundHandler; +import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createPatchLastRevisionHandler; +import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createPatchSpecificRevisionHandler; +import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createPostBinaryHandler; +import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createSearchLastRevisionHandler; +import static org.apache.jackrabbit.oak.remote.http.handler.Handlers.createSearchSpecificRevisionHandler; +import static org.apache.jackrabbit.oak.remote.http.matcher.Matchers.matchesRequest; + +public class RemoteServlet extends HttpServlet { + + private static final Logger logger = LoggerFactory.getLogger(RemoteServlet.class); + + private final RemoteRepository repository; + + public RemoteServlet(RemoteRepository repository) { + this.repository = repository; + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + request.setAttribute("repository", repository); + + try { + firstMatching(readHandlers(), request, createNotFoundHandler()).handle(request, response); + } catch (ServletException e) { + logger.error("unable to serve the current request", e); + throw e; + } catch (IOException e) { + logger.error("I/O error while serving the current request", e); + throw e; + } catch (Exception e) { + logger.error("unexpected error while serving the current request", e); + throw new ServletException(e); + } + } + + private Handler firstMatching(Iterable handlers, HttpServletRequest request, Handler otherwise) { + for (RemoteHandler handler : handlers) { + if (handler.match(request)) { + return handler; + } + } + + return otherwise; + } + + private Iterable readHandlers() { + return handlers( + handler("get", "/revisions/last", createGetLastRevisionHandler()), + handler("get", "/revisions/last/tree/.*", createGetLastTreeHandler()), + handler("head", "/revisions/last/tree/.*", createHeadLastTreeHandler()), + handler("get", "/revisions/[^/]+/tree/.*", createGetRevisionTreeHandler()), + handler("head", "/revisions/[^/]+/tree/.*", createHeadRevisionTreeHandler()), + handler("head", "/binaries/.*", createHeadBinaryHandler()), + handler("get", "/binaries/.*", createGetBinaryHandler()), + handler("post", "/binaries", createPostBinaryHandler()), + handler("patch", "/revisions/last/tree", createPatchLastRevisionHandler()), + handler("patch", "/revisions/[^/]+/tree", createPatchSpecificRevisionHandler()), + handler("get", "/revisions/last/tree", createSearchLastRevisionHandler()), + handler("get", "/revisions/[^/]+/tree", createSearchSpecificRevisionHandler()) + ); + } + + private Iterable handlers(RemoteHandler... handlers) { + return Arrays.asList(handlers); + } + + private RemoteHandler handler(String method, String path, Handler handler) { + return new RemoteHandler(matchesRequest(method, path), handler); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/AuthenticationWrapperHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/AuthenticationWrapperHandler.java new file mode 100644 index 0000000..0f14a96 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/AuthenticationWrapperHandler.java @@ -0,0 +1,182 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import org.apache.jackrabbit.oak.remote.RemoteCredentials; +import org.apache.jackrabbit.oak.remote.RemoteLoginException; +import org.apache.jackrabbit.oak.remote.RemoteRepository; +import org.apache.jackrabbit.oak.remote.RemoteSession; +import org.apache.jackrabbit.util.Base64; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendInternalServerError; + +class AuthenticationWrapperHandler implements Handler { + + private static final Logger logger = LoggerFactory.getLogger(AuthenticationWrapperHandler.class); + + private final Handler authenticated; + + private final Handler notAuthenticated; + + public AuthenticationWrapperHandler(Handler authenticated, Handler notAuthenticated) { + this.authenticated = authenticated; + this.notAuthenticated = notAuthenticated; + } + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + RemoteSession session = (RemoteSession) request.getAttribute("session"); + + if (session != null) { + authenticated.handle(request, response); + return; + } + + RemoteRepository repository = (RemoteRepository) request.getAttribute("repository"); + + if (repository == null) { + sendInternalServerError(response, "repository not found"); + return; + } + + RemoteCredentials credentials = extractCredentials(request, repository); + + if (credentials == null) { + notAuthenticated.handle(request, response); + return; + } + + try { + session = repository.login(credentials); + } catch (RemoteLoginException e) { + logger.warn("unable to authenticate to the repository", e); + notAuthenticated.handle(request, response); + return; + } + + request.setAttribute("session", session); + + authenticated.handle(request, response); + } + + private RemoteCredentials extractCredentials(HttpServletRequest request, RemoteRepository repository) { + String authorization = request.getHeader("Authorization"); + + if (authorization == null) { + return null; + } + + String scheme = getScheme(authorization); + + if (!scheme.equalsIgnoreCase("basic")) { + return null; + } + + String token = getToken(authorization); + + if (token == null) { + return null; + } + + String decoded; + + try { + decoded = Base64.decode(token); + } catch (IllegalArgumentException e) { + return null; + } + + String user = getUser(decoded); + + if (user == null) { + return null; + } + + String password = getPassword(decoded); + + if (password == null) { + return null; + } + + return repository.createBasicCredentials(user, password.toCharArray()); + } + + private String getScheme(String authorization) { + int index = authorization.indexOf(' '); + + if (index < 0) { + return authorization; + } + + return authorization.substring(0, index); + } + + private String getToken(String authorization) { + int index = authorization.indexOf(' '); + + if (index < 0) { + return null; + } + + while (index < authorization.length()) { + if (authorization.charAt(index) != ' ') { + break; + } + + index += 1; + } + + if (index < authorization.length()) { + return authorization.substring(index); + } + + return null; + } + + private String getUser(String both) { + int index = both.indexOf(':'); + + if (index < 0) { + return null; + } + + return both.substring(0, index); + } + + private String getPassword(String both) { + int index = both.indexOf(':'); + + if (index < 0) { + return null; + } + + if (index + 1 < both.length()) { + return both.substring(index + 1); + } + + return null; + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetBinaryHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetBinaryHandler.java new file mode 100644 index 0000000..0e48e8c --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetBinaryHandler.java @@ -0,0 +1,319 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import com.google.common.io.ByteStreams; +import org.apache.jackrabbit.oak.remote.RemoteBinaryFilters; +import org.apache.jackrabbit.oak.remote.RemoteBinaryId; +import org.apache.jackrabbit.oak.remote.RemoteSession; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.StringTokenizer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendBadRequest; +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendInternalServerError; +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendNotFound; + +class GetBinaryHandler implements Handler { + + private static final String CONTENT_RANGE_HEADER = "Content-Range"; + + private static final String RANGE_HEADER = "Range"; + + private static final Pattern RANGE_HEADER_PATTERN = Pattern.compile("^\\s*bytes\\s*=\\s*(.*)\\s*$"); + + private static final Pattern RANGE_PATTERN = Pattern.compile("^\\s*(\\d*)\\s*(?:\\s*-\\s*(\\d*))?\\s*$"); + + private static final String MULTIPART_DELIMITER = "MULTIPART-DELIMITER"; + + private static final Pattern REQUEST_PATTERN = Pattern.compile("^/binaries/(.*)$"); + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + RemoteSession session = (RemoteSession) request.getAttribute("session"); + + if (session == null) { + sendInternalServerError(response, "session not found"); + return; + } + + String providedBinaryId = readBinaryId(request); + + if (providedBinaryId == null) { + sendBadRequest(response, "unable to read the provided binary ID"); + return; + } + + RemoteBinaryId binaryId = session.readBinaryId(providedBinaryId); + + if (binaryId == null) { + sendNotFound(response, "binary ID not found"); + return; + } + + List contentRanges = parseRequestRanges(request, session, binaryId); + + if (contentRanges == null) { + handleFile(response, session, binaryId); + } else if (contentRanges.size() == 1) { + handleSingleRange(response, session, binaryId, contentRanges.get(0)); + } else { + handleMultipleRanges(response, session, binaryId, contentRanges); + } + } + + /** + * RFC7233 + *

+ * This handler sends a 200 OK http status, the Content-Length header and + * the entire file/binary content. This is used when the request Range + * header is missing or it contains a malformed value. + */ + private void handleFile(HttpServletResponse response, RemoteSession session, RemoteBinaryId binaryId) throws IOException { + + InputStream in = session.readBinary(binaryId, new RemoteBinaryFilters()); + + long length = session.readBinaryLength(binaryId); + + response.setStatus(HttpServletResponse.SC_OK); + response.setContentType("application/octet-stream"); + response.setContentLength((int) length); + + OutputStream out = response.getOutputStream(); + + ByteStreams.copy(in, out); + + out.close(); + } + + /** + * RFC7233 + *

+ * This handler sends a 206 Partial Content http status, the Content-Length + * header, the Content-Range header and the requested binary fragment. This + * is used when the request Range header contains only one range. + */ + private void handleSingleRange(HttpServletResponse response, RemoteSession session, RemoteBinaryId binaryId, RemoteBinaryFilters range) throws IOException { + InputStream in = session.readBinary(binaryId, range); + + long fileLength = session.readBinaryLength(binaryId); + long rangeStart = range.getStart(); + long rangeEnd = rangeStart + range.getCount() - 1; + + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + response.setHeader(CONTENT_RANGE_HEADER, String.format("%d-%d/%d", rangeStart, rangeEnd, fileLength)); + response.setContentType("application/octet-stream"); + response.setContentLength((int) (rangeEnd - rangeStart + 1)); + + OutputStream out = response.getOutputStream(); + + ByteStreams.copy(in, out); + + out.close(); + } + + /** + * RFC7233 + *

+ * This handler sends a 206 Partial Content http status, the Content-Length + * header, Content-Type multipart/byteranges The payload contains all the + * requested binary fragments. + *

+ * This handler is used when multiple ranges are requested. + */ + private void handleMultipleRanges(HttpServletResponse response, RemoteSession session, RemoteBinaryId binaryId, List ranges) throws IOException { + + String header; + + long rangeStart, rangeEnd, fileLength, contentLength; + + fileLength = session.readBinaryLength(binaryId); + + // Compute response content length + // Create multipart headers + + contentLength = 0; + + List multipartHeaders = new ArrayList(ranges.size()); + + for (RemoteBinaryFilters range : ranges) { + rangeStart = range.getStart(); + rangeEnd = rangeStart + range.getCount() - 1; + + header = String.format("\n" + + "--%s\n" + + "Content-Type: application/octet-stream" + + "Content-Content-Range: %d-%d/%d\n\n", + MULTIPART_DELIMITER, rangeStart, rangeEnd, fileLength); + + multipartHeaders.add(header); + + contentLength += header.getBytes().length; + contentLength += range.getCount(); + } + + // Send response status and headers + + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + response.setContentLength((int) contentLength); + response.setContentType("multipart/byteranges; boundary=" + MULTIPART_DELIMITER); + + // Send requested ranges + + RemoteBinaryFilters range; + + InputStream in; + + OutputStream out = response.getOutputStream(); + + Iterator rangeIt = ranges.iterator(); + Iterator headerIt = multipartHeaders.iterator(); + + while (rangeIt.hasNext() && headerIt.hasNext()) { + range = rangeIt.next(); + header = headerIt.next(); + + out.write(header.getBytes()); + in = session.readBinary(binaryId, range); + ByteStreams.copy(in, out); + } + + out.close(); + } + + /** + * Extract binary id from request path and return it + */ + private String readBinaryId(HttpServletRequest request) { + Matcher matcher = REQUEST_PATTERN.matcher(request.getPathInfo()); + + if (matcher.matches()) { + return matcher.group(1); + } + + throw new IllegalStateException("handler bound at the wrong path"); + } + + /** + * This method parses the request Range header a list of ranges as + * RemoteBinaryFilters ( or null when the header is missing or contains + * invalid/malformed values + */ + private List parseRequestRanges(HttpServletRequest request, RemoteSession session, RemoteBinaryId binaryId) { + + // Check header exists + String headerValue = request.getHeader(RANGE_HEADER); + + if (headerValue == null) { + return null; + } + + // Check header is bytes=* + Matcher matcher = RANGE_HEADER_PATTERN.matcher(headerValue); + + if (!matcher.matches()) { + return null; + } + + // Iterate requested ranges + headerValue = matcher.group(1); + + StringTokenizer tokenizer = new StringTokenizer(headerValue, ","); + + List ranges = new LinkedList(); + + RemoteBinaryFilters range; + + long fileLength = session.readBinaryLength(binaryId); + + while (tokenizer.hasMoreTokens()) { + range = parseRange(tokenizer.nextToken(), fileLength); + + if (range == null) { + return null; + } + + ranges.add(range); + } + + return ranges; + } + + /** + * Parse a range extracted from the Range header and return a wrapped + * RemoteBinaryFilters instance for the range or null if the range is not + * valid or malformed. + *

+ * The returned RemoteBinaryFilters object will never return -1 in + * getCount. + */ + private RemoteBinaryFilters parseRange(String range, long fileLength) { + Matcher matcher = RANGE_PATTERN.matcher(range); + + if (!matcher.matches()) { + return null; + } + + final long start; + final long end; + + // Content-Range: X + if (matcher.group(2) == null || matcher.group(2).isEmpty()) { + start = Long.parseLong(matcher.group(1)); + end = fileLength - 1; + } + // Content-Range: -X + else if (matcher.group(1).isEmpty()) { + end = fileLength - 1; + start = end - Long.parseLong(matcher.group(2)) + 1; + } + // Content-Range: X-Y + else { + start = Long.parseLong(matcher.group(1)); + end = Long.parseLong(matcher.group(2)); + } + + // Simple range validation + if (start < 0 || end < 0 || start > end || end >= fileLength || start >= fileLength) { + return null; + } + + return new RemoteBinaryFilters() { + @Override + public long getStart() { + return start; + } + + @Override + public long getCount() { + return end - start + 1; + } + }; + } +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastRevisionHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastRevisionHandler.java new file mode 100644 index 0000000..9f828cf --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastRevisionHandler.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import org.apache.jackrabbit.oak.remote.RemoteRevision; +import org.apache.jackrabbit.oak.remote.RemoteSession; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendInternalServerError; + +class GetLastRevisionHandler implements Handler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException { + RemoteSession session = (RemoteSession) request.getAttribute("session"); + + if (session == null) { + sendInternalServerError(response, "session not found"); + return; + } + + RemoteRevision revision = session.readLastRevision(); + + response.setStatus(HttpServletResponse.SC_OK); + response.setContentType("application/json"); + + ServletOutputStream stream = response.getOutputStream(); + + JsonGenerator generator = new JsonFactory().createJsonGenerator(stream, JsonEncoding.UTF8); + generator.writeStartObject(); + generator.writeStringField("revision", revision.asString()); + generator.writeEndObject(); + generator.flush(); + + stream.close(); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastTreeHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastTreeHandler.java new file mode 100644 index 0000000..dd6ef8d --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetLastTreeHandler.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import org.apache.jackrabbit.oak.remote.RemoteRevision; +import org.apache.jackrabbit.oak.remote.RemoteSession; + +import javax.servlet.http.HttpServletRequest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class GetLastTreeHandler extends GetTreeHandler { + + private static final Pattern REQUEST_PATTERN = Pattern.compile("^/revisions/last/tree(/.*)$"); + + protected String readPath(HttpServletRequest request) { + Matcher matcher = REQUEST_PATTERN.matcher(request.getPathInfo()); + + if (matcher.matches()) { + return matcher.group(1); + } + + throw new IllegalStateException("handler bound at the wrong path"); + } + + @Override + protected RemoteRevision readRevision(HttpServletRequest request, RemoteSession session) { + return session.readLastRevision(); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetRevisionTreeHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetRevisionTreeHandler.java new file mode 100644 index 0000000..ee8ef8c --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetRevisionTreeHandler.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import org.apache.jackrabbit.oak.remote.RemoteRevision; +import org.apache.jackrabbit.oak.remote.RemoteSession; + +import javax.servlet.http.HttpServletRequest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class GetRevisionTreeHandler extends GetTreeHandler { + + private static final Pattern REQUEST_PATTERN = Pattern.compile("^/revisions/([^/]+)/tree(/.*)$"); + + @Override + protected String readPath(HttpServletRequest request) { + Matcher matcher = REQUEST_PATTERN.matcher(request.getPathInfo()); + + if (matcher.matches()) { + return matcher.group(2); + } + + throw new IllegalStateException("handler bound at the wrong path"); + } + + @Override + protected RemoteRevision readRevision(HttpServletRequest request, RemoteSession session) { + Matcher matcher = REQUEST_PATTERN.matcher(request.getPathInfo()); + + if (matcher.matches()) { + return session.readRevision(matcher.group(1)); + } + + throw new IllegalStateException("handler bound at the wrong path"); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetTreeHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetTreeHandler.java new file mode 100644 index 0000000..3d91e15 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/GetTreeHandler.java @@ -0,0 +1,264 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.google.common.collect.Sets; +import org.apache.jackrabbit.oak.remote.RemoteRevision; +import org.apache.jackrabbit.oak.remote.RemoteSession; +import org.apache.jackrabbit.oak.remote.RemoteTree; +import org.apache.jackrabbit.oak.remote.RemoteTreeFilters; +import org.apache.jackrabbit.oak.remote.RemoteValue; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +import static java.util.Collections.singletonMap; +import static org.apache.jackrabbit.oak.remote.http.handler.RemoteValues.renderJson; +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendGone; +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendInternalServerError; +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendNotFound; + +abstract class GetTreeHandler implements Handler { + + protected abstract String readPath(HttpServletRequest request); + + protected abstract RemoteRevision readRevision(HttpServletRequest request, RemoteSession session); + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException { + RemoteSession session = (RemoteSession) request.getAttribute("session"); + + if (session == null) { + sendInternalServerError(response, "session not found"); + return; + } + + RemoteRevision revision = readRevision(request, session); + + if (revision == null) { + sendGone(response, "unable to read the revision"); + return; + } + + RemoteTree tree = session.readTree(revision, readPath(request), readFilters(request)); + + if (tree == null) { + sendNotFound(response, singletonMap("Oak-Revision", revision.asString()), "tree not found"); + return; + } + + response.setStatus(HttpServletResponse.SC_OK); + response.setHeader("Oak-Revision", revision.asString()); + response.setContentType("application/json"); + + ServletOutputStream stream = response.getOutputStream(); + + JsonGenerator generator = new JsonFactory().createJsonGenerator(stream, JsonEncoding.UTF8); + renderResponse(generator, tree); + generator.flush(); + + stream.close(); + } + + private void renderResponse(JsonGenerator generator, RemoteTree tree) throws IOException { + if (tree == null) { + generator.writeNull(); + } else { + generator.writeStartObject(); + generator.writeFieldName("properties"); + renderProperties(generator, tree.getProperties()); + generator.writeFieldName("children"); + renderChildren(generator, tree.getChildren()); + generator.writeFieldName("hasMoreChildren"); + generator.writeBoolean(tree.hasMoreChildren()); + generator.writeEndObject(); + } + } + + private void renderChildren(JsonGenerator generator, Map children) throws IOException { + generator.writeStartObject(); + + for (Map.Entry entry : children.entrySet()) { + generator.writeFieldName(entry.getKey()); + renderResponse(generator, entry.getValue()); + } + + generator.writeEndObject(); + } + + private void renderProperties(JsonGenerator generator, Map properties) throws IOException { + generator.writeStartObject(); + + for (Map.Entry entry : properties.entrySet()) { + generator.writeFieldName(entry.getKey()); + renderJson(generator, entry.getValue()); + } + + generator.writeEndObject(); + } + + private RemoteTreeFilters readFilters(final HttpServletRequest request) { + return new RemoteTreeFilters() { + + @Override + public int getDepth() { + Integer depth = readDepth(request); + + if (depth == null) { + return super.getDepth(); + } + + return depth; + } + + @Override + public Set getPropertyFilters() { + Set propertyFilters = readPropertyFilters(request); + + if (propertyFilters == null) { + return super.getPropertyFilters(); + } + + return propertyFilters; + } + + @Override + public Set getNodeFilters() { + Set nodeFilters = readNodeFilters(request); + + if (nodeFilters == null) { + return super.getNodeFilters(); + } + + return nodeFilters; + } + + @Override + public long getBinaryThreshold() { + Long binaryThreshold = readBinaryThreshold(request); + + if (binaryThreshold == null) { + return super.getBinaryThreshold(); + } + + return binaryThreshold; + } + + @Override + public int getChildrenStart() { + Integer childrenStart = readChildrenStart(request); + + if (childrenStart == null) { + return super.getChildrenStart(); + } + + return childrenStart; + } + + @Override + public int getChildrenCount() { + Integer childrenCount = readChildrenCount(request); + + if (childrenCount == null) { + return super.getChildrenCount(); + } + + return childrenCount; + } + + }; + } + + private Integer readDepth(HttpServletRequest request) { + return readIntegerParameter(request, "depth"); + } + + private Set readPropertyFilters(HttpServletRequest request) { + return readSetParameter(request, "properties"); + } + + private Set readNodeFilters(HttpServletRequest request) { + return readSetParameter(request, "children"); + } + + private Long readBinaryThreshold(HttpServletRequest request) { + return readLongParameter(request, "binaries"); + } + + private Integer readChildrenStart(HttpServletRequest request) { + return readIntegerParameter(request, "childrenStart"); + } + + private Integer readChildrenCount(HttpServletRequest request) { + return readIntegerParameter(request, "childrenCount"); + } + + private Integer readIntegerParameter(HttpServletRequest request, String name) { + String value = request.getParameter(name); + + if (value == null) { + return null; + } + + Integer result; + + try { + result = Integer.parseInt(value, 10); + } catch (NumberFormatException e) { + result = null; + } + + return result; + } + + private Long readLongParameter(HttpServletRequest request, String name) { + String value = request.getParameter(name); + + if (value == null) { + return null; + } + + Long result; + + try { + result = Long.parseLong(value, 10); + } catch (NumberFormatException e) { + result = null; + } + + return result; + } + + private Set readSetParameter(HttpServletRequest request, String name) { + String[] values = request.getParameterValues(name); + + if (values == null) { + return null; + } + + return Sets.newHashSet(values); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/Handler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/Handler.java new file mode 100644 index 0000000..79bda5f --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/Handler.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public interface Handler { + + void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException; + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/Handlers.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/Handlers.java new file mode 100644 index 0000000..6336cb2 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/Handlers.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +/** + * A collection of handlers used to respond to some requests handled by the + * remote servlet. + */ +public class Handlers { + + private Handlers() { + } + + /** + * Create an handler that will return the last revision available to the + * server. + * + * @return An instance of {@code Handler}. + */ + public static Handler createGetLastRevisionHandler() { + return withAuthentication(new GetLastRevisionHandler()); + } + + /** + * Create an handler that will return a repository sub-tree at a specific + * revision. + * + * @return An instance of {@code Handler}. + */ + public static Handler createGetRevisionTreeHandler() { + return withAuthentication(new GetRevisionTreeHandler()); + } + + /** + * Create an handler that will return a repository sub-tree at the latest + * known state. + * + * @return An instance of {@code Handler}. + */ + public static Handler createGetLastTreeHandler() { + return withAuthentication(new GetLastTreeHandler()); + } + + /** + * Create an handler that will return a 404 response to the client. + * + * @return An instance of {@code Handler}. + */ + public static Handler createNotFoundHandler() { + return new NotFoundHandler(); + } + + /** + * Create a handler that will read a binary object from the repository. + * + * @return An instance of {@code Handler}. + */ + public static Handler createGetBinaryHandler() { + return withAuthentication(new GetBinaryHandler()); + } + + /** + * Create a handler that will check if a binary exists + * + * @return An instance of {@code Handler} + */ + public static Handler createHeadBinaryHandler() { + return withAuthentication(new HeadBinaryHandler()); + } + + /** + * Create a handler that will perform the creation of new binary object. + * + * @return An instance of {@code Handler}. + */ + public static Handler createPostBinaryHandler() { + return withAuthentication(new PostBinaryHandler()); + } + + /** + * Create a handler that will patch the content at a specific revision. + * + * @return An instance of {@code Handler}. + */ + public static Handler createPatchSpecificRevisionHandler() { + return withAuthentication(new PatchSpecificRevisionHandler()); + } + + /** + * Create a handler that will patch the content at the last revision. + * + * @return An instance of {@code Handler}. + */ + public static Handler createPatchLastRevisionHandler() { + return withAuthentication(new PatchLastRevisionHandler()); + } + + /** + * Create a handler that checks if a tree exists at the last revision. + * + * @return An instance of {@code Handler}. + */ + public static Handler createHeadLastTreeHandler() { + return withAuthentication(new HeadLastTreeHandler()); + } + + /** + * Create a handler that checks if a tree exists at a given revision. + * + * @return An instance of {@code Handler}. + */ + public static Handler createHeadRevisionTreeHandler() { + return withAuthentication(new HeadRevisionTreeHandler()); + } + + /** + * Create a handler that searches for content at the last revision. + * + * @return An instance of {@code Handler}. + */ + public static Handler createSearchLastRevisionHandler() { + return withAuthentication(new SearchLastRevisionHandler()); + } + + /** + * Create a handler that searches for content at the provided revision. + * + * @return An instance of {@code Handler}. + */ + public static Handler createSearchSpecificRevisionHandler() { + return withAuthentication(new SearchSpecificRevisionHandler()); + } + + private static Handler withAuthentication(Handler authenticated) { + return new AuthenticationWrapperHandler(authenticated, createForbiddenHandler()); + } + + private static Handler createForbiddenHandler() { + return new UnauthorizedHandler(); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadBinaryHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadBinaryHandler.java new file mode 100644 index 0000000..db13b47 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadBinaryHandler.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import org.apache.jackrabbit.oak.remote.RemoteBinaryId; +import org.apache.jackrabbit.oak.remote.RemoteSession; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendBadRequest; +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendInternalServerError; +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendNotFound; + +class HeadBinaryHandler implements Handler { + + private static final Pattern REQUEST_PATTERN = Pattern.compile("^/binaries/(.*)$"); + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + RemoteSession session = (RemoteSession) request.getAttribute("session"); + + if (session == null) { + sendInternalServerError(response, "session not found"); + return; + } + + String providedBinaryId = readBinaryId(request); + + if (providedBinaryId == null) { + sendBadRequest(response, "unable to read the provided binary ID"); + return; + } + + RemoteBinaryId binaryId = session.readBinaryId(providedBinaryId); + + if (binaryId == null) { + sendNotFound(response, "binary ID not found"); + return; + } + + response.setStatus(HttpServletResponse.SC_OK); + response.setHeader("Accept-Ranges", "bytes"); + } + + /** + * Extract binary id from request path and return it + */ + private String readBinaryId(HttpServletRequest request) { + Matcher matcher = REQUEST_PATTERN.matcher(request.getPathInfo()); + + if (matcher.matches()) { + return matcher.group(1); + } + + throw new IllegalStateException("handler bound at the wrong path"); + } +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadLastTreeHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadLastTreeHandler.java new file mode 100644 index 0000000..9b1a45c --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadLastTreeHandler.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import org.apache.jackrabbit.oak.remote.RemoteRevision; +import org.apache.jackrabbit.oak.remote.RemoteSession; + +import javax.servlet.http.HttpServletRequest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class HeadLastTreeHandler extends HeadTreeHandler { + + private static final Pattern REQUEST_PATTERN = Pattern.compile("^/revisions/last/tree(/.*)$"); + + protected String readPath(HttpServletRequest request) { + Matcher matcher = REQUEST_PATTERN.matcher(request.getPathInfo()); + + if (matcher.matches()) { + return matcher.group(1); + } + + throw new IllegalStateException("handler bound at the wrong path"); + } + + @Override + protected RemoteRevision readRevision(HttpServletRequest request, RemoteSession session) { + return session.readLastRevision(); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadRevisionTreeHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadRevisionTreeHandler.java new file mode 100644 index 0000000..aa9efc1 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadRevisionTreeHandler.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import org.apache.jackrabbit.oak.remote.RemoteRevision; +import org.apache.jackrabbit.oak.remote.RemoteSession; + +import javax.servlet.http.HttpServletRequest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class HeadRevisionTreeHandler extends HeadTreeHandler { + + private static final Pattern REQUEST_PATTERN = Pattern.compile("^/revisions/([^/]+)/tree(/.*)$"); + + @Override + protected String readPath(HttpServletRequest request) { + Matcher matcher = REQUEST_PATTERN.matcher(request.getPathInfo()); + + if (matcher.matches()) { + return matcher.group(2); + } + + throw new IllegalStateException("handler bound at the wrong path"); + } + + @Override + protected RemoteRevision readRevision(HttpServletRequest request, RemoteSession session) { + Matcher matcher = REQUEST_PATTERN.matcher(request.getPathInfo()); + + if (matcher.matches()) { + return session.readRevision(matcher.group(1)); + } + + throw new IllegalStateException("handler bound at the wrong path"); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadTreeHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadTreeHandler.java new file mode 100644 index 0000000..b5a785c --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/HeadTreeHandler.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import org.apache.jackrabbit.oak.remote.RemoteRevision; +import org.apache.jackrabbit.oak.remote.RemoteSession; +import org.apache.jackrabbit.oak.remote.RemoteTree; +import org.apache.jackrabbit.oak.remote.RemoteTreeFilters; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static java.util.Collections.singletonMap; +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendGone; +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendInternalServerError; +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendNotFound; + +abstract class HeadTreeHandler implements Handler { + + protected abstract String readPath(HttpServletRequest request); + + protected abstract RemoteRevision readRevision(HttpServletRequest request, RemoteSession session); + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException { + RemoteSession session = (RemoteSession) request.getAttribute("session"); + + if (session == null) { + sendInternalServerError(response, "session not found"); + return; + } + + RemoteRevision revision = readRevision(request, session); + + if (revision == null) { + sendGone(response, "revision not found"); + return; + } + + RemoteTree tree = session.readTree(revision, readPath(request), new RemoteTreeFilters()); + + if (tree == null) { + sendNotFound(response, singletonMap("Oak-Revision", revision.asString()), "tree not found"); + return; + } + + response.setStatus(HttpServletResponse.SC_OK); + response.setHeader("Oak-Revision", revision.asString()); + response.setContentType("application/json"); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/NotFoundHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/NotFoundHandler.java new file mode 100644 index 0000000..339c038 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/NotFoundHandler.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendNotFound; + +class NotFoundHandler implements Handler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException { + sendNotFound(response, "requested path not found"); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchLastRevisionHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchLastRevisionHandler.java new file mode 100644 index 0000000..6df4132 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchLastRevisionHandler.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import org.apache.jackrabbit.oak.remote.RemoteRevision; +import org.apache.jackrabbit.oak.remote.RemoteSession; + +import javax.servlet.http.HttpServletRequest; + +class PatchLastRevisionHandler extends PatchRevisionHandler { + + @Override + protected RemoteRevision readRevision(HttpServletRequest request, RemoteSession session) { + return session.readLastRevision(); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchRevisionHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchRevisionHandler.java new file mode 100644 index 0000000..ff86b28 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchRevisionHandler.java @@ -0,0 +1,445 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.jackrabbit.oak.remote.RemoteCommitException; +import org.apache.jackrabbit.oak.remote.RemoteOperation; +import org.apache.jackrabbit.oak.remote.RemoteRevision; +import org.apache.jackrabbit.oak.remote.RemoteSession; +import org.apache.jackrabbit.oak.remote.RemoteValue; +import org.apache.jackrabbit.oak.remote.RemoteValue.Supplier; +import org.apache.jackrabbit.util.Base64; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendBadRequest; +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendGone; +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendInternalServerError; + +abstract class PatchRevisionHandler implements Handler { + + private static final Logger logger = LoggerFactory.getLogger(PatchRevisionHandler.class); + + protected abstract RemoteRevision readRevision(HttpServletRequest request, RemoteSession session); + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException { + RemoteSession session = (RemoteSession) request.getAttribute("session"); + + if (session == null) { + sendInternalServerError(response, "session not found"); + return; + } + + RemoteRevision base = readRevision(request, session); + + if (base == null) { + sendGone(response, "revision not found"); + return; + } + + RemoteOperation operation; + + try { + operation = parseOperations(session, new ObjectMapper().readTree(request.getInputStream())); + } catch (Exception e) { + operation = null; + } + + if (operation == null) { + sendBadRequest(response, "unable to parse the list of operations"); + return; + } + + RemoteRevision revision; + + try { + revision = session.commit(base, operation); + } catch (RemoteCommitException e) { + logger.warn("unable to perform the commit", e); + sendBadRequest(response, "commit failed"); + return; + } + + response.setStatus(HttpServletResponse.SC_CREATED); + response.setContentType("application/json"); + + ServletOutputStream stream = response.getOutputStream(); + + JsonGenerator generator = new JsonFactory().createJsonGenerator(stream, JsonEncoding.UTF8); + renderResponse(generator, revision); + generator.flush(); + + stream.close(); + } + + private void renderResponse(JsonGenerator generator, RemoteRevision revision) throws IOException { + generator.writeStartObject(); + generator.writeStringField("revision", revision.asString()); + generator.writeEndObject(); + } + + private RemoteOperation parseOperations(RemoteSession session, JsonNode json) { + List operations = new ArrayList(); + + for (JsonNode child : json) { + operations.add(parseOperation(session, child)); + } + + return session.createAggregateOperation(operations); + } + + private RemoteOperation parseOperation(RemoteSession session, JsonNode json) { + String type = parseStringField(json, "op"); + + if (type.equals("add")) { + return parseAddOperation(session, json); + } + + if (type.equals("remove")) { + return parseRemoveOperation(session, json); + } + + if (type.equals("set")) { + return parseSetOperation(session, json); + } + + if (type.equals("unset")) { + return parseUnsetOperation(session, json); + } + + if (type.equals("copy")) { + return parseCopyOperation(session, json); + } + + if (type.equals("move")) { + return parseMoveOperation(session, json); + } + + throw new IllegalArgumentException("invalid operation type"); + } + + private RemoteOperation parseMoveOperation(RemoteSession session, JsonNode node) { + return session.createMoveOperation(parseStringField(node, "from"), parseStringField(node, "to")); + } + + private RemoteOperation parseCopyOperation(RemoteSession session, JsonNode node) { + return session.createCopyOperation(parseStringField(node, "from"), parseStringField(node, "to")); + } + + private RemoteOperation parseUnsetOperation(RemoteSession session, JsonNode node) { + return session.createUnsetOperation(parseStringField(node, "path"), parseStringField(node, "name")); + } + + private RemoteOperation parseSetOperation(RemoteSession session, JsonNode node) { + return session.createSetOperation(parseStringField(node, "path"), parseStringField(node, "name"), parseValue(node)); + } + + private RemoteOperation parseRemoveOperation(RemoteSession session, JsonNode node) { + return session.createRemoveOperation(parseStringField(node, "path")); + } + + private RemoteOperation parseAddOperation(RemoteSession session, JsonNode json) { + return session.createAddOperation(parseStringField(json, "path"), parsePropertiesField(json, "properties")); + } + + private Map parsePropertiesField(JsonNode node, String name) { + return parseProperties(node.get(name)); + } + + private Map parseProperties(JsonNode node) { + Map values = new HashMap(); + + Iterator> iterator = node.fields(); + + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + values.put(entry.getKey(), parseValue(entry.getValue())); + } + + return values; + } + + private RemoteValue parseValue(JsonNode node) { + String type = parseStringField(node, "type"); + + if (type.equals("string")) { + return RemoteValue.toText(parseStringField(node, "value")); + } + + if (type.equals("strings")) { + return RemoteValue.toMultiText(parseStringArrayField(node, "value")); + } + + if (type.equals("binary")) { + return RemoteValue.toBinary(parseBinaryField(node, "value")); + } + + if (type.equals("binaries")) { + return RemoteValue.toMultiBinary(parseBinaryArrayField(node, "value")); + } + + if (type.equals("binaryId")) { + return RemoteValue.toBinaryId(parseStringField(node, "value")); + } + + if (type.equals("binaryIds")) { + return RemoteValue.toMultiBinaryId(parseStringArrayField(node, "value")); + } + + if (type.equals("long")) { + return RemoteValue.toLong(parseLongField(node, "value")); + } + + if (type.equals("longs")) { + return RemoteValue.toMultiLong(parseLongArrayField(node, "value")); + } + + if (type.equals("double")) { + return RemoteValue.toDouble(parseDoubleField(node, "value")); + } + + if (type.equals("doubles")) { + return RemoteValue.toMultiDouble(parseDoubleArrayField(node, "value")); + } + + if (type.equals("date")) { + return RemoteValue.toDate(parseLongField(node, "value")); + } + + if (type.equals("dates")) { + return RemoteValue.toMultiDate(parseLongArrayField(node, "value")); + } + + if (type.equals("boolean")) { + return RemoteValue.toBoolean(parseBooleanField(node, "value")); + } + + if (type.equals("booleans")) { + return RemoteValue.toMultiBoolean(parseBooleanArrayField(node, "value")); + } + + if (type.equals("name")) { + return RemoteValue.toName(parseStringField(node, "value")); + } + + if (type.equals("names")) { + return RemoteValue.toMultiName(parseStringArrayField(node, "value")); + } + + if (type.equals("path")) { + return RemoteValue.toPath(parseStringField(node, "value")); + } + + if (type.equals("paths")) { + return RemoteValue.toMultiPath(parseStringArrayField(node, "value")); + } + + if (type.equals("reference")) { + return RemoteValue.toReference(parseStringField(node, "value")); + } + + if (type.equals("references")) { + return RemoteValue.toMultiReference(parseStringArrayField(node, "value")); + } + + if (type.equals("weakReference")) { + return RemoteValue.toWeakReference(parseStringField(node, "value")); + } + + if (type.equals("weakReferences")) { + return RemoteValue.toMultiWeakReference(parseStringArrayField(node, "value")); + } + + if (type.equals("uri")) { + return RemoteValue.toUri(parseStringField(node, "value")); + } + + if (type.equals("uris")) { + return RemoteValue.toMultiUri(parseStringArrayField(node, "value")); + } + + if (type.equals("decimal")) { + return RemoteValue.toDecimal(parseDecimalField(node, "value")); + } + + if (type.equals("decimals")) { + return RemoteValue.toMultiDecimal(parseDecimalArrayField(node, "value")); + } + + throw new IllegalArgumentException("invalid value type"); + } + + private BigDecimal parseDecimalField(JsonNode node, String field) { + return parseDecimal(node.get(field)); + } + + private BigDecimal parseDecimal(JsonNode node) { + return new BigDecimal(node.asText()); + } + + private Iterable parseDecimalArrayField(JsonNode node, String field) { + return parseDecimalArray(node.get(field)); + } + + private Iterable parseDecimalArray(JsonNode node) { + List result = new ArrayList(); + + for (JsonNode element : node) { + result.add(parseDecimal(element)); + } + + return result; + } + + private boolean parseBooleanField(JsonNode node, String field) { + return parseBoolean(node.get(field)); + } + + private boolean parseBoolean(JsonNode node) { + return node.asBoolean(); + } + + private Iterable parseBooleanArrayField(JsonNode node, String field) { + return parseBooleanArray(node.get(field)); + } + + private Iterable parseBooleanArray(JsonNode node) { + List result = new ArrayList(); + + for (JsonNode element : node) { + result.add(parseBoolean(element)); + } + + return result; + } + + private double parseDoubleField(JsonNode node, String field) { + return parseDouble(node.get(field)); + } + + private double parseDouble(JsonNode node) { + return node.asDouble(); + } + + private Iterable parseDoubleArrayField(JsonNode node, String field) { + return parseDoubleArray(node.get(field)); + } + + private Iterable parseDoubleArray(JsonNode node) { + List result = new ArrayList(); + + for (JsonNode element : node) { + result.add(parseDouble(element)); + } + + return result; + } + + private long parseLongField(JsonNode node, String field) { + return parseLong(node.get(field)); + } + + private long parseLong(JsonNode node) { + return node.asLong(); + } + + private Iterable parseLongArrayField(JsonNode node, String field) { + return parseLongArray(node.get(field)); + } + + private Iterable parseLongArray(JsonNode node) { + List result = new ArrayList(); + + for (JsonNode element : node) { + result.add(parseLong(element)); + } + + return result; + } + + private Supplier parseBinaryField(JsonNode node, String field) { + return parseBinary(node.get(field)); + } + + private Supplier parseBinary(final JsonNode node) { + return new Supplier() { + + @Override + public InputStream get() { + return new ByteArrayInputStream(Base64.decode(node.asText()).getBytes()); + } + + }; + } + + private Iterable> parseBinaryArrayField(JsonNode node, String field) { + return parseBinaryArray(node.get(field)); + } + + private Iterable> parseBinaryArray(JsonNode node) { + List> result = new ArrayList>(); + + for (JsonNode element : node) { + result.add(parseBinary(element)); + } + + return result; + } + + private Iterable parseStringArrayField(JsonNode node, String field) { + return parseStringArray(node.get(field)); + } + + private String parseStringField(JsonNode node, String field) { + return parseString(node.get(field)); + } + + private Iterable parseStringArray(JsonNode node) { + List result = new ArrayList(); + + for (JsonNode element : node) { + result.add(parseString(element)); + } + + return result; + } + + private String parseString(JsonNode node) { + return node.asText(); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchSpecificRevisionHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchSpecificRevisionHandler.java new file mode 100644 index 0000000..0696c8d --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PatchSpecificRevisionHandler.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import org.apache.jackrabbit.oak.remote.RemoteRevision; +import org.apache.jackrabbit.oak.remote.RemoteSession; + +import javax.servlet.http.HttpServletRequest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class PatchSpecificRevisionHandler extends PatchRevisionHandler { + + private static final Pattern REQUEST_PATTERN = Pattern.compile("^/revisions/([^/]+)/tree$"); + + @Override + protected RemoteRevision readRevision(HttpServletRequest request, RemoteSession session) { + Matcher matcher = REQUEST_PATTERN.matcher(request.getPathInfo()); + + if (matcher.matches()) { + return session.readRevision(matcher.group(1)); + } + + throw new IllegalStateException("handler bound at the wrong path"); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PostBinaryHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PostBinaryHandler.java new file mode 100644 index 0000000..37ec159 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/PostBinaryHandler.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import org.apache.jackrabbit.oak.remote.RemoteBinaryId; +import org.apache.jackrabbit.oak.remote.RemoteSession; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendInternalServerError; + +class PostBinaryHandler implements Handler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException { + RemoteSession session = (RemoteSession) request.getAttribute("session"); + + if (session == null) { + sendInternalServerError(response, "session not found"); + return; + } + + RemoteBinaryId binaryId = session.writeBinary(request.getInputStream()); + + response.setStatus(HttpServletResponse.SC_CREATED); + response.setContentType("application/json"); + + ServletOutputStream stream = response.getOutputStream(); + + JsonGenerator generator = new JsonFactory().createJsonGenerator(stream, JsonEncoding.UTF8); + generator.writeStartObject(); + generator.writeStringField("binaryId", binaryId.asString()); + generator.writeEndObject(); + generator.flush(); + + stream.close(); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/RemoteValues.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/RemoteValues.java new file mode 100644 index 0000000..6c90500 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/RemoteValues.java @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.google.common.io.BaseEncoding; +import com.google.common.io.ByteStreams; +import org.apache.jackrabbit.oak.remote.RemoteValue; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; + +class RemoteValues { + + private RemoteValues() { + // Prevent instantiation + } + + public static void renderJsonOrNull(JsonGenerator generator, RemoteValue value) throws IOException { + if (value == null) { + generator.writeNull(); + } else { + renderJson(generator, value); + } + } + + public static void renderJson(JsonGenerator generator, RemoteValue value) throws IOException { + if (value.isBinary()) { + renderValue(generator, "binary", value.asBinary(), getBinaryWriter()); + } + + if (value.isMultiBinary()) { + renderMultiValue(generator, "binaries", value.asMultiBinary(), getBinaryWriter()); + } + + if (value.isBinaryId()) { + renderValue(generator, "binaryId", value.asBinaryId(), getStringWriter()); + } + + if (value.isMultiBinaryId()) { + renderMultiValue(generator, "binaryIds", value.asMultiBinaryId(), getStringWriter()); + } + + if (value.isBoolean()) { + renderValue(generator, "boolean", value.asBoolean(), getBooleanWriter()); + } + + if (value.isMultiBoolean()) { + renderMultiValue(generator, "booleans", value.asMultiBoolean(), getBooleanWriter()); + } + + if (value.isDate()) { + renderValue(generator, "date", value.asDate(), getLongWriter()); + } + + if (value.isMultiDate()) { + renderMultiValue(generator, "dates", value.asMultiDate(), getLongWriter()); + } + + if (value.isDecimal()) { + renderValue(generator, "decimal", value.asDecimal(), getDecimalWriter()); + } + + if (value.isMultiDecimal()) { + renderMultiValue(generator, "decimals", value.asMultiDecimal(), getDecimalWriter()); + } + + if (value.isDouble()) { + renderValue(generator, "double", value.asDouble(), getDoubleWriter()); + } + + if (value.isMultiDouble()) { + renderMultiValue(generator, "doubles", value.asMultiDouble(), getDoubleWriter()); + } + + if (value.isLong()) { + renderValue(generator, "long", value.asLong(), getLongWriter()); + } + + if (value.isMultiLong()) { + renderMultiValue(generator, "longs", value.asMultiLong(), getLongWriter()); + } + + if (value.isName()) { + renderValue(generator, "name", value.asName(), getStringWriter()); + } + + if (value.isMultiName()) { + renderMultiValue(generator, "names", value.asMultiName(), getStringWriter()); + } + + if (value.isPath()) { + renderValue(generator, "path", value.asPath(), getStringWriter()); + } + + if (value.isMultiPath()) { + renderMultiValue(generator, "paths", value.asMultiPath(), getStringWriter()); + } + + if (value.isReference()) { + renderValue(generator, "reference", value.asReference(), getStringWriter()); + } + + if (value.isMultiReference()) { + renderMultiValue(generator, "references", value.asMultiReference(), getStringWriter()); + } + + if (value.isText()) { + renderValue(generator, "string", value.asText(), getStringWriter()); + } + + if (value.isMultiText()) { + renderMultiValue(generator, "strings", value.asMultiText(), getStringWriter()); + } + + if (value.isUri()) { + renderValue(generator, "uri", value.asUri(), getStringWriter()); + } + + if (value.isMultiUri()) { + renderMultiValue(generator, "uris", value.asMultiUri(), getStringWriter()); + } + + if (value.isWeakReference()) { + renderValue(generator, "weakReference", value.asWeakReference(), getStringWriter()); + } + + if (value.isMultiWeakReference()) { + renderMultiValue(generator, "weakReferences", value.asMultiWeakReference(), getStringWriter()); + } + } + + private static GeneratorWriter> getBinaryWriter() { + return new GeneratorWriter>() { + + @Override + public void write(JsonGenerator generator, RemoteValue.Supplier value) throws IOException { + generator.writeString(BaseEncoding.base64().encode(ByteStreams.toByteArray(value.get()))); + } + + }; + } + + private static GeneratorWriter getStringWriter() { + return new GeneratorWriter() { + + @Override + public void write(JsonGenerator generator, String value) throws IOException { + generator.writeString(value); + } + + }; + } + + private static GeneratorWriter getBooleanWriter() { + return new GeneratorWriter() { + + @Override + public void write(JsonGenerator generator, Boolean value) throws IOException { + generator.writeBoolean(value); + } + + }; + } + + private static GeneratorWriter getLongWriter() { + return new GeneratorWriter() { + + @Override + public void write(JsonGenerator generator, Long value) throws IOException { + generator.writeNumber(value); + } + + }; + } + + private static GeneratorWriter getDecimalWriter() { + return new GeneratorWriter() { + + @Override + public void write(JsonGenerator generator, BigDecimal value) throws IOException { + generator.writeString(value.toString()); + } + + }; + } + + private static GeneratorWriter getDoubleWriter() { + return new GeneratorWriter() { + + @Override + public void write(JsonGenerator generator, Double value) throws IOException { + generator.writeNumber(value); + } + + }; + } + + private static void renderValue(JsonGenerator generator, String type, T value, GeneratorWriter writer) throws IOException { + generator.writeStartObject(); + generator.writeStringField("type", type); + generator.writeFieldName("value"); + + writer.write(generator, value); + + generator.writeEndObject(); + } + + private static void renderMultiValue(JsonGenerator generator, String type, Iterable values, GeneratorWriter writer) throws IOException { + generator.writeStartObject(); + generator.writeStringField("type", type); + generator.writeArrayFieldStart("value"); + + for (T value : values) { + writer.write(generator, value); + } + + generator.writeEndArray(); + generator.writeEndObject(); + } + + + private interface GeneratorWriter { + + void write(JsonGenerator generator, T value) throws IOException; + + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/ResponseUtils.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/ResponseUtils.java new file mode 100644 index 0000000..603af40 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/ResponseUtils.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +class ResponseUtils { + + private ResponseUtils() { + } + + private static void send(HttpServletResponse response, int code, String message) throws IOException { + send(response, code, new HashMap(), message); + } + + private static void send(HttpServletResponse response, int code, Map headers, String message) throws IOException { + response.setStatus(code); + response.setContentType("application/json"); + + for (Map.Entry entry : headers.entrySet()) { + response.setHeader(entry.getKey(), entry.getValue()); + } + + ServletOutputStream stream = response.getOutputStream(); + + JsonGenerator generator = new JsonFactory().createJsonGenerator(stream, JsonEncoding.UTF8); + generator.writeStartObject(); + generator.writeStringField("error", message); + generator.writeEndObject(); + generator.flush(); + + stream.close(); + } + + public static void sendBadRequest(HttpServletResponse response, String message) throws IOException { + send(response, HttpServletResponse.SC_BAD_REQUEST, message); + } + + public static void sendInternalServerError(HttpServletResponse response, String message) throws IOException { + send(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, message); + } + + public static void sendGone(HttpServletResponse response, String message) throws IOException { + send(response, HttpServletResponse.SC_GONE, message); + } + + public static void sendNotFound(HttpServletResponse response, String message) throws IOException { + send(response, HttpServletResponse.SC_NOT_FOUND, message); + } + + public static void sendNotFound(HttpServletResponse response, Map headers, String message) throws IOException { + send(response, HttpServletResponse.SC_NOT_FOUND, headers, message); + } + + public static void sendUnauthorized(HttpServletResponse response, String message) throws IOException { + send(response, HttpServletResponse.SC_UNAUTHORIZED, message); + } + + public static void sendUnauthorized(HttpServletResponse response, Map headers, String message) throws IOException { + send(response, HttpServletResponse.SC_UNAUTHORIZED, headers, message); + } + + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchLastRevisionHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchLastRevisionHandler.java new file mode 100644 index 0000000..ddabb87 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchLastRevisionHandler.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import org.apache.jackrabbit.oak.remote.RemoteRevision; +import org.apache.jackrabbit.oak.remote.RemoteSession; + +import javax.servlet.http.HttpServletRequest; + +class SearchLastRevisionHandler extends SearchRevisionHandler { + + @Override + protected RemoteRevision readRevision(HttpServletRequest request, RemoteSession session) { + return session.readLastRevision(); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchRevisionHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchRevisionHandler.java new file mode 100644 index 0000000..8636dba --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchRevisionHandler.java @@ -0,0 +1,209 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import org.apache.jackrabbit.oak.remote.RemoteQueryParseException; +import org.apache.jackrabbit.oak.remote.RemoteResult; +import org.apache.jackrabbit.oak.remote.RemoteResults; +import org.apache.jackrabbit.oak.remote.RemoteRevision; +import org.apache.jackrabbit.oak.remote.RemoteSession; + +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.apache.jackrabbit.oak.remote.http.handler.RemoteValues.renderJson; +import static org.apache.jackrabbit.oak.remote.http.handler.RemoteValues.renderJsonOrNull; +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendBadRequest; +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendGone; +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendInternalServerError; + +abstract class SearchRevisionHandler implements Handler { + + protected abstract RemoteRevision readRevision(HttpServletRequest request, RemoteSession session); + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + RemoteSession session = (RemoteSession) request.getAttribute("session"); + + if (session == null) { + sendInternalServerError(response, "session not found"); + return; + } + + RemoteRevision revision = readRevision(request, session); + + if (revision == null) { + sendGone(response, "unable to read the revision"); + return; + } + + String query = readQuery(request); + + if (query == null) { + sendBadRequest(response, "query not specified"); + return; + } + + String language = readLanguage(request); + + if (language == null) { + sendBadRequest(response, "language not specified"); + return; + } + + Long offset = readOffset(request); + + if (offset == null) { + sendBadRequest(response, "offset not specified"); + return; + } + + Long limit = readLimit(request); + + if (limit == null) { + sendBadRequest(response, "limit not specified"); + return; + } + + RemoteResults results; + + try { + results = session.search(revision, query, language, offset, limit); + } catch (RemoteQueryParseException e) { + sendBadRequest(response, "malformed query"); + return; + } + + response.setStatus(HttpServletResponse.SC_OK); + response.setHeader("Oak-Revision", revision.asString()); + response.setContentType("application/json"); + + ServletOutputStream stream = response.getOutputStream(); + + JsonGenerator generator = new JsonFactory().createJsonGenerator(stream, JsonEncoding.UTF8); + renderResponse(generator, results); + generator.flush(); + + stream.close(); + } + + private String readQuery(HttpServletRequest request) { + return readStringParameter(request, "query"); + } + + private String readLanguage(HttpServletRequest request) { + return readStringParameter(request, "language"); + } + + private String readStringParameter(HttpServletRequest request, String name) { + return request.getParameter(name); + } + + private Long readOffset(HttpServletRequest request) { + return readLongParameter(request, "offset"); + } + + private Long readLimit(HttpServletRequest request) { + return readLongParameter(request, "limit"); + } + + private Long readLongParameter(HttpServletRequest request, String name) { + String value = readStringParameter(request, name); + + if (value == null) { + return null; + } + + try { + return Long.parseLong(value, 10); + } catch (NumberFormatException e) { + return null; + } + } + + private void renderResponse(JsonGenerator generator, RemoteResults results) throws IOException { + generator.writeStartObject(); + generator.writeFieldName("total"); + generator.writeNumber(results.getTotal()); + generator.writeFieldName("columns"); + renderStrings(generator, results.getColumns()); + generator.writeFieldName("selectors"); + renderStrings(generator, results.getSelectors()); + generator.writeFieldName("results"); + renderResults(generator, results); + generator.writeEndObject(); + } + + private void renderStrings(JsonGenerator generator, Iterable elements) throws IOException { + generator.writeStartArray(); + + for (String element : elements) { + generator.writeString(element); + } + + generator.writeEndArray(); + } + + private void renderResults(JsonGenerator generator, RemoteResults results) throws IOException { + generator.writeStartArray(); + + for (RemoteResult result : results) { + renderResult(generator, results, result); + } + + generator.writeEndArray(); + } + + private void renderResult(JsonGenerator generator, RemoteResults results, RemoteResult result) throws IOException { + generator.writeStartObject(); + generator.writeFieldName("columns"); + renderColumns(generator, results, result); + generator.writeFieldName("selectors"); + renderSelectors(generator, results, result); + generator.writeEndObject(); + } + + private void renderColumns(JsonGenerator generator, RemoteResults results, RemoteResult result) throws IOException { + generator.writeStartObject(); + + for (String name : results.getColumns()) { + generator.writeFieldName(name); + renderJsonOrNull(generator, result.getColumnValue(name)); + } + + generator.writeEndObject(); + } + + private void renderSelectors(JsonGenerator generator, RemoteResults results, RemoteResult result) throws IOException { + generator.writeStartObject(); + + for (String name : results.getSelectors()) { + generator.writeFieldName(name); + generator.writeString(result.getSelectorPath(name)); + } + + generator.writeEndObject(); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchSpecificRevisionHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchSpecificRevisionHandler.java new file mode 100644 index 0000000..208c12f --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/SearchSpecificRevisionHandler.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import org.apache.jackrabbit.oak.remote.RemoteRevision; +import org.apache.jackrabbit.oak.remote.RemoteSession; + +import javax.servlet.http.HttpServletRequest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class SearchSpecificRevisionHandler extends SearchRevisionHandler { + + private static final Pattern REQUEST_PATTERN = Pattern.compile("^/revisions/([^/]+)/tree$"); + + @Override + protected RemoteRevision readRevision(HttpServletRequest request, RemoteSession session) { + Matcher matcher = REQUEST_PATTERN.matcher(request.getPathInfo()); + + if (matcher.matches()) { + return session.readRevision(matcher.group(1)); + } + + throw new IllegalStateException("handler bound at the wrong path"); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/UnauthorizedHandler.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/UnauthorizedHandler.java new file mode 100644 index 0000000..a04145c --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/handler/UnauthorizedHandler.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collections; + +import static org.apache.jackrabbit.oak.remote.http.handler.ResponseUtils.sendUnauthorized; + +class UnauthorizedHandler implements Handler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException { + sendUnauthorized(response, Collections.singletonMap("WWW-Authenticate", "Basic realm=\"Oak\""), "unable to authenticate"); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/AllMatcher.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/AllMatcher.java new file mode 100644 index 0000000..aebb192 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/AllMatcher.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.matcher; + +import javax.servlet.http.HttpServletRequest; + +class AllMatcher implements Matcher { + + private final Matcher[] matchers; + + public AllMatcher(Matcher... matchers) { + this.matchers = matchers; + } + + @Override + public boolean match(HttpServletRequest request) { + for (Matcher matcher : matchers) { + if (!matcher.match(request)) { + return false; + } + } + + return true; + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matcher.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matcher.java new file mode 100644 index 0000000..a551833 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matcher.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.matcher; + +import javax.servlet.http.HttpServletRequest; + +/** + * A predicate over an HTTP request. This predicate can be used to check if some + * preconditions on the request are met. + */ +public interface Matcher { + + /** + * Check if the preconditions on the given request are met. + * + * @param request Request to check. + * @return {@code true} if the preconditions are met, {@code false} + * otherwise. + */ + boolean match(HttpServletRequest request); + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matchers.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matchers.java new file mode 100644 index 0000000..c609d22 --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/Matchers.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.matcher; + +import java.util.regex.Pattern; + +/** + * Collection of matchers for HTTP requests. + */ +public class Matchers { + + private Matchers() { + } + + /** + * Create a matcher that will be satisfied when given requests have a method + * matching the one provided as a parameter. + * + * @param method Method that requests must have for the matcher to be + * satisfied. + * @return An instance of {@code Matcher}. + */ + public static Matcher matchesMethod(String method) { + if (method == null) { + throw new IllegalArgumentException("method not provided"); + } + + return new MethodMatcher(method); + } + + /** + * Create a matcher that will be satisfied when given requests have a patch + * matching the pattern provided as a parameter. + * + * @param pattern The pattern to use when checking the requests given to the + * matcher. + * @return An instance of {@code Matcher}. + */ + public static Matcher matchesPath(String pattern) { + if (pattern == null) { + throw new IllegalArgumentException("pattern not provided"); + } + + return new PathMatcher(Pattern.compile(pattern)); + } + + /** + * Create a matcher that will be satisfied when the given requests satisfies + * every matcher provided as parameters. Calling this method is equivalent + * as checking every provided matcher individually and chaining each result + * as a short-circuit and. + * + * @param matchers The matchers that have to be satisfied for the returned + * matcher to be satisfied. + * @return An instance of {@code Matcher}. + */ + public static Matcher matchesAll(Matcher... matchers) { + if (matchers == null) { + throw new IllegalArgumentException("matchers not provided"); + } + + for (Matcher matcher : matchers) { + if (matcher == null) { + throw new IllegalArgumentException("invalid matcher"); + } + } + + return new AllMatcher(matchers); + } + + /** + * Create a matcher that will be satisifed when the given requests match the + * provided method and path. + * + * @param method The method that requests must have for the matcher to be + * satisfied. + * @param path The pattern to use when checking the requests given to the + * matcher. + * @return An instance of {@code Matcher}. + */ + public static Matcher matchesRequest(String method, String path) { + return matchesAll(matchesMethod(method), matchesPath(path)); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/MethodMatcher.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/MethodMatcher.java new file mode 100644 index 0000000..6ce44ff --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/MethodMatcher.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.matcher; + +import javax.servlet.http.HttpServletRequest; + +class MethodMatcher implements Matcher { + + private final String method; + + public MethodMatcher(String method) { + this.method = method; + } + + @Override + public boolean match(HttpServletRequest request) { + return request.getMethod().toLowerCase().equals(method.toLowerCase()); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/PathMatcher.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/PathMatcher.java new file mode 100644 index 0000000..d8d04af --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/http/matcher/PathMatcher.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.matcher; + +import javax.servlet.http.HttpServletRequest; +import java.util.regex.Pattern; + +class PathMatcher implements Matcher { + + private final Pattern pattern; + + public PathMatcher(Pattern pattern) { + this.pattern = pattern; + } + + @Override + public boolean match(HttpServletRequest request) { + String requestPath = request.getPathInfo(); + + if (requestPath == null) { + return false; + } + + return pattern.matcher(requestPath).matches(); + } + +} diff --git oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/osgi/RemoteServletRegistration.java oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/osgi/RemoteServletRegistration.java new file mode 100644 index 0000000..39e64fd --- /dev/null +++ oak-remote/src/main/java/org/apache/jackrabbit/oak/remote/osgi/RemoteServletRegistration.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.osgi; + +import org.apache.felix.scr.annotations.Activate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Deactivate; +import org.apache.felix.scr.annotations.Properties; +import org.apache.felix.scr.annotations.Property; +import org.apache.felix.scr.annotations.Reference; +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.remote.RemoteRepository; +import org.apache.jackrabbit.oak.remote.content.ContentRemoteRepository; +import org.apache.jackrabbit.oak.remote.http.RemoteServlet; +import org.osgi.service.http.HttpService; + +import java.util.Map; + +import static org.apache.felix.scr.annotations.ConfigurationPolicy.REQUIRE; + +@Component( + metatype = true, + policy = REQUIRE, + label = "Apache Jackrabbit Oak Remote HTTP API", + description = "The HTTP binding of the Remote API for a Jackrabbit Oak repository" +) +@Properties({ + @Property( + name = "url", + value = {"/api"}, + label = "Mount URL", + description = "Where the root application is exposed in the URL namespace" + ) +}) +public class RemoteServletRegistration { + + @Reference + private HttpService httpService; + + @Reference + private ContentRepository contentRepository; + + @Activate + public void activate(Map properties) { + registerServlet(getUrl(properties)); + } + + @Deactivate + public void deactivate(Map properties) { + unregisterServlet(getUrl(properties)); + } + + private void registerServlet(String url) { + try { + httpService.registerServlet(url, getRemoteServlet(), null, null); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void unregisterServlet(String url) { + httpService.unregister(url); + } + + private String getUrl(Map properties) { + return (String) properties.get("url"); + } + + private RemoteServlet getRemoteServlet() { + return new RemoteServlet(getRemoteRepository()); + } + + private RemoteRepository getRemoteRepository() { + return new ContentRemoteRepository(contentRepository); + } + +} diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/AddContentRemoteOperationTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/AddContentRemoteOperationTest.java new file mode 100644 index 0000000..6ab5c75 --- /dev/null +++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/AddContentRemoteOperationTest.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.remote.RemoteCommitException; +import org.junit.Test; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class AddContentRemoteOperationTest { + + public AddContentRemoteOperation createOperation(String path) { + return new AddContentRemoteOperation(path); + } + + @Test + public void testAddNode() throws Exception { + Tree parent = mock(Tree.class); + doReturn(true).when(parent).exists(); + + Tree tree = mock(Tree.class); + doReturn(false).when(tree).exists(); + doReturn(parent).when(tree).getParent(); + doReturn("test").when(tree).getName(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test").apply(root); + + verify(parent).addChild("test"); + } + + @Test(expected = RemoteCommitException.class) + public void testAddNodeWithExistingTree() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test").apply(root); + } + + @Test(expected = RemoteCommitException.class) + public void testAddNodeWithNonExistingParent() throws Exception { + Tree parent = mock(Tree.class); + doReturn(false).when(parent).exists(); + + Tree tree = mock(Tree.class); + doReturn(false).when(tree).exists(); + doReturn(parent).when(tree).getParent(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test").apply(root); + } + +} diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRepositoryTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRepositoryTest.java new file mode 100644 index 0000000..e243841 --- /dev/null +++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteRepositoryTest.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import com.google.common.collect.Sets; +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.remote.RemoteCredentials; +import org.apache.jackrabbit.oak.remote.RemoteLoginException; +import org.junit.Ignore; +import org.junit.Test; + +import javax.jcr.Credentials; +import javax.security.auth.login.LoginException; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ContentRemoteRepositoryTest { + + private ContentRemoteRepository createRepository() { + return createRepository(mock(ContentRepository.class)); + } + + private ContentRemoteRepository createRepository(ContentRepository repository) { + return new ContentRemoteRepository(repository); + } + + @Test + public void testCreateBasicCredentials() { + assertNotNull(createRepository().createBasicCredentials("admin", "admin".toCharArray())); + } + + @Test + @Ignore + public void testCreateImpersonationCredentials() { + assertNotNull(createRepository().createImpersonationCredentials(Sets.newHashSet("admin"))); + } + + @Test(expected = IllegalArgumentException.class) + public void testLoginWithNullCredentials() throws Exception { + createRepository().login(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testLoginWithInvalidCredentials() throws Exception { + createRepository().login(mock(RemoteCredentials.class)); + } + + @Test + public void testSuccessfulLoginWithBasicCredentials() throws Exception { + ContentRepository repository = mock(ContentRepository.class); + when(repository.login(any(Credentials.class), anyString())).thenReturn(mock(ContentSession.class)); + + ContentRemoteRepository remoteRepository = createRepository(repository); + assertNotNull(remoteRepository.login(remoteRepository.createBasicCredentials("admin", "admin".toCharArray()))); + } + + @Test(expected = RemoteLoginException.class) + public void testUnsuccessfulLoginWithBasicCredentials() throws Exception { + ContentRepository repository = mock(ContentRepository.class); + when(repository.login(any(Credentials.class), anyString())).thenThrow(LoginException.class); + + ContentRemoteRepository remoteRepository = createRepository(repository); + remoteRepository.login(remoteRepository.createBasicCredentials("admin", "admin".toCharArray())); + } + +} diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResultTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResultTest.java new file mode 100644 index 0000000..4900eee --- /dev/null +++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResultTest.java @@ -0,0 +1,449 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.ResultRow; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.remote.RemoteValue; +import org.apache.jackrabbit.util.ISO8601; +import org.junit.Test; + +import java.math.BigDecimal; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +import static com.google.common.collect.Lists.newArrayList; +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; + +public class ContentRemoteResultTest { + + private ContentRemoteResult createResult(ResultRow row) { + return new ContentRemoteResult(mock(ContentRemoteBinaries.class), row); + } + + private ContentRemoteResult createResult(ContentRemoteBinaries binaries, ResultRow row) { + return new ContentRemoteResult(binaries, row); + } + + @Test(expected = IllegalArgumentException.class) + public void testColumnNotAvailable() { + ResultRow row = mock(ResultRow.class); + doThrow(IllegalArgumentException.class).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + result.getColumnValue("column"); + } + + @Test + public void testStringColumn() { + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.STRING).when(value).getType(); + doReturn("value").when(value).getValue(Type.STRING); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals("value", remoteValue.asText()); + } + + @Test + public void testBinaryColumn() { + Blob blob = mock(Blob.class); + + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.BINARY).when(value).getType(); + doReturn(blob).when(value).getValue(Type.BINARY); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteBinaries binaries = mock(ContentRemoteBinaries.class); + doReturn("id").when(binaries).put(blob); + + ContentRemoteResult result = createResult(binaries, row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals("id", remoteValue.asBinaryId()); + } + + @Test + public void testLongColumn() { + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.LONG).when(value).getType(); + doReturn(42L).when(value).getValue(Type.LONG); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals(42L, remoteValue.asLong().longValue()); + } + + @Test + public void testDoubleColumn() { + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.DOUBLE).when(value).getType(); + doReturn(4.2).when(value).getValue(Type.DOUBLE); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals(4.2, remoteValue.asDouble().doubleValue(), 1e-5); + } + + @Test + public void testDateColumn() { + Date now = new Date(); + + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.DATE).when(value).getType(); + doReturn(toFormattedDate(now)).when(value).getValue(Type.DATE); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals(now.getTime(), remoteValue.asDate().longValue()); + } + + @Test + public void testBooleanColumn() { + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.BOOLEAN).when(value).getType(); + doReturn(true).when(value).getValue(Type.BOOLEAN); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals(true, remoteValue.asBoolean()); + } + + @Test + public void testNameColumn() { + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.NAME).when(value).getType(); + doReturn("value").when(value).getValue(Type.NAME); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals("value", remoteValue.asName()); + } + + @Test + public void testPathColumn() { + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.PATH).when(value).getType(); + doReturn("value").when(value).getValue(Type.PATH); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals("value", remoteValue.asPath()); + } + + @Test + public void testReferenceColumn() { + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.REFERENCE).when(value).getType(); + doReturn("value").when(value).getValue(Type.REFERENCE); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals("value", remoteValue.asReference()); + } + + @Test + public void testWeakReferenceColumn() { + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.WEAKREFERENCE).when(value).getType(); + doReturn("value").when(value).getValue(Type.WEAKREFERENCE); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals("value", remoteValue.asWeakReference()); + } + + @Test + public void testUriColumn() { + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.URI).when(value).getType(); + doReturn("value").when(value).getValue(Type.URI); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals("value", remoteValue.asUri()); + } + + @Test + public void testDecimalColumn() { + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.DECIMAL).when(value).getType(); + doReturn(BigDecimal.ONE).when(value).getValue(Type.DECIMAL); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals(BigDecimal.ONE, remoteValue.asDecimal()); + } + + @Test + public void testMultiStringColumn() { + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.STRINGS).when(value).getType(); + doReturn(asList("a", "b")).when(value).getValue(Type.STRINGS); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals(asList("a", "b"), remoteValue.asMultiText()); + } + + @Test + public void testMultiBinaryColumn() { + Blob first = mock(Blob.class); + Blob second = mock(Blob.class); + + ContentRemoteBinaries binaries = mock(ContentRemoteBinaries.class); + doReturn("first").when(binaries).put(first); + doReturn("second").when(binaries).put(second); + + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.BINARIES).when(value).getType(); + doReturn(asList(first, second)).when(value).getValue(Type.BINARIES); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(binaries, row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals(asList("first", "second"), remoteValue.asMultiBinaryId()); + } + + @Test + public void testMultiLongColumn() { + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.LONGS).when(value).getType(); + doReturn(asList(4L, 2L)).when(value).getValue(Type.LONGS); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals(asList(4L, 2L), remoteValue.asMultiLong()); + } + + @Test + public void testMultiDoubleColumn() { + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.DOUBLES).when(value).getType(); + doReturn(asList(4.0, 2.0)).when(value).getValue(Type.DOUBLES); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals(asList(4.0, 2.0), remoteValue.asMultiDouble()); + } + + @Test + public void testMultiDateColumn() { + Date first = new Date(4); + Date second = new Date(2); + + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.DATES).when(value).getType(); + doReturn(toFormattedDates(first, second)).when(value).getValue(Type.DATES); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals(asList(4L, 2L), remoteValue.asMultiDate()); + } + + @Test + public void testMultiBooleanColumn() { + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.BOOLEANS).when(value).getType(); + doReturn(asList(true, false)).when(value).getValue(Type.BOOLEANS); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals(asList(true, false), remoteValue.asMultiBoolean()); + } + + @Test + public void testMultiNameColumn() { + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.NAMES).when(value).getType(); + doReturn(asList("a", "b")).when(value).getValue(Type.NAMES); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals(asList("a", "b"), remoteValue.asMultiName()); + } + + @Test + public void testMultiPathColumn() { + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.PATHS).when(value).getType(); + doReturn(asList("a", "b")).when(value).getValue(Type.PATHS); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals(asList("a", "b"), remoteValue.asMultiPath()); + } + + @Test + public void testMultiReferenceColumn() { + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.REFERENCES).when(value).getType(); + doReturn(asList("a", "b")).when(value).getValue(Type.REFERENCES); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals(asList("a", "b"), remoteValue.asMultiReference()); + } + + @Test + public void testMultiWeakReferenceColumn() { + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.WEAKREFERENCES).when(value).getType(); + doReturn(asList("a", "b")).when(value).getValue(Type.WEAKREFERENCES); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals(asList("a", "b"), remoteValue.asMultiWeakReference()); + } + + @Test + public void testMultiUriColumn() { + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.URIS).when(value).getType(); + doReturn(asList("a", "b")).when(value).getValue(Type.URIS); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals(asList("a", "b"), remoteValue.asMultiUri()); + } + + @Test + public void testMultiDecimalColumn() { + BigDecimal first = new BigDecimal(4); + BigDecimal second = new BigDecimal(2); + + PropertyValue value = mock(PropertyValue.class); + doReturn(Type.DECIMALS).when(value).getType(); + doReturn(asList(first, second)).when(value).getValue(Type.DECIMALS); + + ResultRow row = mock(ResultRow.class); + doReturn(value).when(row).getValue("column"); + + ContentRemoteResult result = createResult(row); + RemoteValue remoteValue = result.getColumnValue("column"); + assertEquals(asList(first, second), remoteValue.asMultiDecimal()); + } + + @Test(expected = IllegalArgumentException.class) + public void testSelectorNotAvailable() { + ResultRow row = mock(ResultRow.class); + doThrow(IllegalArgumentException.class).when(row).getPath("selector"); + createResult(row).getSelectorPath("selector"); + } + + @Test + public void testSelector() { + ResultRow row = mock(ResultRow.class); + doReturn("path").when(row).getPath("selector"); + assertEquals("path", createResult(row).getSelectorPath("selector")); + } + + private Iterable toFormattedDates(Date... dates) { + List result = newArrayList(); + + for (Date date : dates) { + result.add(toFormattedDate(date)); + } + + return result; + } + + private String toFormattedDate(Date date) { + return ISO8601.format(toCalendar(date)); + } + + private Calendar toCalendar(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(date.getTime()); + return calendar; + } + +} diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResultsTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResultsTest.java new file mode 100644 index 0000000..b54034c --- /dev/null +++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteResultsTest.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.Result; +import org.junit.Test; + +import static com.google.common.collect.Iterables.elementsEqual; +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public class ContentRemoteResultsTest { + + private ContentRemoteResults createContentRemoteResults(Result result) { + return new ContentRemoteResults(createContentRemoteBinaries(), result); + } + + private ContentRemoteBinaries createContentRemoteBinaries() { + return mock(ContentRemoteBinaries.class); + } + + @Test + public void testGetTotal() { + Result result = mock(Result.class); + doReturn(42L).when(result).getSize(); + assertEquals(42L, createContentRemoteResults(result).getTotal()); + } + + @Test + public void testGetColumns() { + String[] columns = {"a", "b"}; + + Result result = mock(Result.class); + doReturn(columns).when(result).getColumnNames(); + + assertTrue(elementsEqual(asList(columns), createContentRemoteResults(result).getColumns())); + } + + @Test + public void testGetSelectors() { + String[] selectors = {"a", "b"}; + + Result result = mock(Result.class); + doReturn(selectors).when(result).getSelectorNames(); + + assertTrue(elementsEqual(asList(selectors), createContentRemoteResults(result).getSelectors())); + } + +} diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteSessionTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteSessionTest.java new file mode 100644 index 0000000..8a2ee81 --- /dev/null +++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteSessionTest.java @@ -0,0 +1,542 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.AuthInfo; +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.remote.RemoteBinaryFilters; +import org.apache.jackrabbit.oak.remote.RemoteBinaryId; +import org.apache.jackrabbit.oak.remote.RemoteCommitException; +import org.apache.jackrabbit.oak.remote.RemoteOperation; +import org.apache.jackrabbit.oak.remote.RemoteRevision; +import org.apache.jackrabbit.oak.remote.RemoteTreeFilters; +import org.apache.jackrabbit.oak.remote.RemoteValue; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ContentRemoteSessionTest { + + private ContentRemoteSession createSession() { + return createSession(mock(ContentSession.class)); + } + + private ContentRemoteSession createSession(ContentRemoteBinaries binaries) { + return new ContentRemoteSession(mock(ContentSession.class), mock(ContentRemoteRevisions.class), binaries); + } + + private ContentRemoteSession createSession(ContentSession session, ContentRemoteBinaries binaries) { + return new ContentRemoteSession(session, mock(ContentRemoteRevisions.class), binaries); + } + + private ContentRemoteSession createSession(ContentSession session, ContentRemoteRevisions revisions) { + return new ContentRemoteSession(session, revisions, mock(ContentRemoteBinaries.class)); + } + + private ContentRemoteSession createSession(ContentSession session) { + return new ContentRemoteSession(session, mock(ContentRemoteRevisions.class), mock(ContentRemoteBinaries.class)); + } + + @Test + public void testReadLastRevision() { + assertNotNull(createSession().readLastRevision()); + } + + @Test + public void testReadLastRevisionAsString() { + Root root = mock(Root.class); + + AuthInfo authInfo = mock(AuthInfo.class); + + ContentSession session = mock(ContentSession.class); + doReturn(authInfo).when(session).getAuthInfo(); + doReturn(root).when(session).getLatestRoot(); + + ContentRemoteRevisions revisions = mock(ContentRemoteRevisions.class); + doReturn("id").when(revisions).put(authInfo, root); + + assertEquals("id", createSession(session, revisions).readLastRevision().asString()); + } + + @Test + public void testReadRevision() { + Root root = mock(Root.class); + + AuthInfo authInfo = mock(AuthInfo.class); + + ContentSession session = mock(ContentSession.class); + doReturn(authInfo).when(session).getAuthInfo(); + + ContentRemoteRevisions revisions = mock(ContentRemoteRevisions.class); + doReturn(root).when(revisions).get(authInfo, "id"); + + assertNotNull(createSession(session, revisions).readRevision("id")); + } + + @Test + public void testReadRevisionAsString() { + Root root = mock(Root.class); + + AuthInfo authInfo = mock(AuthInfo.class); + + ContentSession session = mock(ContentSession.class); + doReturn(authInfo).when(session).getAuthInfo(); + + ContentRemoteRevisions revisions = mock(ContentRemoteRevisions.class); + doReturn(root).when(revisions).get(authInfo, "id"); + + assertEquals("id", createSession(session, revisions).readRevision("id").asString()); + } + + @Test(expected = IllegalArgumentException.class) + public void testReadTreeWithNullRevision() throws Exception { + createSession().readTree(null, "/", new RemoteTreeFilters()); + } + + @Test(expected = IllegalArgumentException.class) + public void testReadTreeWithInvalidRevision() throws Exception { + createSession().readTree(mock(RemoteRevision.class), "/", new RemoteTreeFilters()); + } + + @Test(expected = IllegalArgumentException.class) + public void testReadTreeWithNullPath() throws Exception { + ContentRemoteRevision revision = mock(ContentRemoteRevision.class); + createSession().readTree(revision, null, new RemoteTreeFilters()); + } + + @Test(expected = IllegalArgumentException.class) + public void testReadTreeWithInvalidPath() throws Exception { + ContentRemoteRevision revision = mock(ContentRemoteRevision.class); + createSession().readTree(revision, "invalid", new RemoteTreeFilters()); + } + + @Test(expected = IllegalArgumentException.class) + public void testReadTreeWithNullFilters() throws Exception { + ContentRemoteRevision revision = mock(ContentRemoteRevision.class); + createSession().readTree(revision, "/", null); + } + + @Test + public void testReadNonExistingTree() throws Exception { + Tree tree = mock(Tree.class); + when(tree.exists()).thenReturn(false); + + Root root = mock(Root.class); + when(root.getTree(anyString())).thenReturn(tree); + + ContentRemoteRevision revision = mock(ContentRemoteRevision.class); + doReturn(root).when(revision).getRoot(); + + assertNull(createSession().readTree(revision, "/", new RemoteTreeFilters())); + } + + @Test + public void testReadExistingTree() throws Exception { + Tree tree = mock(Tree.class); + when(tree.exists()).thenReturn(true); + + Root root = mock(Root.class); + when(root.getTree(anyString())).thenReturn(tree); + + ContentRemoteRevision revision = mock(ContentRemoteRevision.class); + doReturn(root).when(revision).getRoot(); + + assertNotNull(createSession().readTree(revision, "/", new RemoteTreeFilters())); + } + + @Test + public void testCreateAddOperation() { + assertNotNull(createSession().createAddOperation("/test", new HashMap())); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateAddOperationWithNullPath() { + createSession().createAddOperation(null, new HashMap()); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateAddOperationWithInvalidPath() { + createSession().createAddOperation("invalid", new HashMap()); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateAddOperationWithRootPath() { + createSession().createAddOperation("/", new HashMap()); + } + + @Test(expected = IllegalArgumentException.class) + public void createAddOperationWithNullProperties() { + createSession().createAddOperation("/test", null); + } + + @Test + public void testCreateRemoveOperation() { + assertNotNull(createSession().createRemoveOperation("/test")); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRemoveOperationWithNullPath() { + createSession().createRemoveOperation(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRemoveOperationWithInvalidPath() { + createSession().createRemoveOperation("invalid"); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRemoveOperationWithRootPath() { + createSession().createRemoveOperation("/"); + } + + @Test + public void testCreateSetOperation() { + assertNotNull(createSession().createSetOperation("/test", "name", RemoteValue.toText("value"))); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateSetOperationWithNullPath() { + createSession().createSetOperation(null, "name", RemoteValue.toText("value")); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateSetOperationWithInvalidPath() { + createSession().createSetOperation("invalid", "name", RemoteValue.toText("value")); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateSetOperationWithNullName() { + createSession().createSetOperation("/test", null, RemoteValue.toText("value")); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateSetOperationWithEmptyName() { + createSession().createSetOperation("/test", "", RemoteValue.toText("value")); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateSetOperationWithNullValue() { + createSession().createSetOperation("/test", "name", null); + } + + @Test + public void testCreateUnsetOperation() { + assertNotNull(createSession().createUnsetOperation("/test", "name")); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateUnsetOperationWithNullPath() { + createSession().createUnsetOperation(null, "name"); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateUnsetOperationWithInvalidPath() { + createSession().createUnsetOperation("invalid", "name"); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateUnsetOperationWithNullName() { + createSession().createUnsetOperation("/test", null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateUnsetOperationWithEmptyName() { + createSession().createUnsetOperation("/test", ""); + } + + @Test + public void createCopyOperation() { + assertNotNull(createSession().createCopyOperation("/source", "/target")); + } + + @Test(expected = IllegalArgumentException.class) + public void createCopyOperationWithNullSourcePath() { + createSession().createCopyOperation(null, "/target"); + } + + @Test(expected = IllegalArgumentException.class) + public void createCopyOperationWithInvalidSourcePath() { + createSession().createCopyOperation("invalid", "/target"); + } + + @Test(expected = IllegalArgumentException.class) + public void createCopyOperationWithNullTargetPath() { + createSession().createCopyOperation("/source", null); + } + + @Test(expected = IllegalArgumentException.class) + public void createCopyOperationWithInvalidTargetPath() { + createSession().createCopyOperation("/source", "invalid"); + } + + @Test(expected = IllegalArgumentException.class) + public void createCopyOperationWithSameSourceAndTargetPath() { + createSession().createCopyOperation("/same", "/same"); + } + + @Test(expected = IllegalArgumentException.class) + public void createCopyOperationWithSourceAncestorOfTarget() { + createSession().createCopyOperation("/source", "/source/target"); + } + + @Test + public void createMoveOperation() { + assertNotNull(createSession().createMoveOperation("/source", "/target")); + } + + @Test(expected = IllegalArgumentException.class) + public void createMoveOperationWithNullSourcePath() { + createSession().createMoveOperation(null, "/target"); + } + + @Test(expected = IllegalArgumentException.class) + public void createMoveOperationWithInvalidSourcePath() { + createSession().createMoveOperation("invalid", "/target"); + } + + @Test(expected = IllegalArgumentException.class) + public void createMoveOperationWithNullTargetPath() { + createSession().createMoveOperation("/source", null); + } + + @Test(expected = IllegalArgumentException.class) + public void createMoveOperationWithInvalidTargetPath() { + createSession().createMoveOperation("/source", "invalid"); + } + + @Test(expected = IllegalArgumentException.class) + public void createMoveOperationWithSameSourceAndTargetPath() { + createSession().createMoveOperation("/same", "/same"); + } + + @Test(expected = IllegalArgumentException.class) + public void createMoveOperationWithSourceAncestorOfTarget() { + createSession().createMoveOperation("/source", "/source/target"); + } + + @Test + public void createAggregateOperation() { + assertNotNull(createSession().createAggregateOperation(new ArrayList())); + } + + @Test(expected = IllegalArgumentException.class) + public void createAggregateOperationWithNullList() { + createSession().createAggregateOperation(null); + } + + @Test + public void testCommit() throws Exception { + Root root = mock(Root.class); + + ContentRemoteOperation operation = mock(ContentRemoteOperation.class); + + ContentRemoteRevision revision = mock(ContentRemoteRevision.class); + doReturn(root).when(revision).getRoot(); + + assertNotNull(createSession().commit(revision, operation)); + } + + @Test(expected = IllegalArgumentException.class) + public void testCommitWithNullRevision() throws Exception { + createSession().commit(null, mock(ContentRemoteOperation.class)); + } + + @Test(expected = IllegalArgumentException.class) + public void testCommitWithInvalidRevision() throws Exception { + createSession().commit(mock(RemoteRevision.class), mock(ContentRemoteOperation.class)); + } + + @Test(expected = IllegalArgumentException.class) + public void testCommitWithNullOperation() throws Exception { + createSession().commit(mock(ContentRemoteRevision.class), null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCommitWithInvalidOperation() throws Exception { + createSession().commit(mock(ContentRemoteRevision.class), mock(RemoteOperation.class)); + } + + @Test(expected = RemoteCommitException.class) + public void testCommitWithOperationThrowingException() throws Exception { + Root root = mock(Root.class); + + ContentRemoteOperation operation = mock(ContentRemoteOperation.class); + doThrow(RemoteCommitException.class).when(operation).apply(root); + + ContentRemoteRevision revision = mock(ContentRemoteRevision.class); + doReturn(root).when(revision).getRoot(); + + createSession().commit(revision, operation); + } + + @Test(expected = RemoteCommitException.class) + public void testCommitWithConflictingCommit() throws Exception { + Root root = mock(Root.class); + doThrow(CommitFailedException.class).when(root).commit(); + + ContentRemoteRevision revision = mock(ContentRemoteRevision.class); + doReturn(root).when(revision).getRoot(); + + createSession().commit(revision, mock(ContentRemoteOperation.class)); + } + + @Test + public void testReadBinaryId() { + Blob blob = mock(Blob.class); + + ContentRemoteBinaries binaries = mock(ContentRemoteBinaries.class); + doReturn(blob).when(binaries).get("id"); + + ContentRemoteSession remoteSession = createSession(binaries); + assertNotNull(remoteSession.readBinaryId("id")); + } + + @Test + public void testReadBinaryIdAsString() { + Blob blob = mock(Blob.class); + + ContentRemoteBinaries binaries = mock(ContentRemoteBinaries.class); + doReturn(blob).when(binaries).get("id"); + + ContentRemoteSession remoteSession = createSession(binaries); + assertEquals("id", remoteSession.readBinaryId("id").asString()); + } + + @Test(expected = IllegalArgumentException.class) + public void testReadBinaryIdWithNullReference() { + createSession().readBinaryId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testReadBinaryIdWithEmptyReference() { + createSession().readBinaryId(""); + } + + @Test + public void testReadBinaryIdWithInvalidReference() { + Root root = mock(Root.class); + doReturn(null).when(root).getBlob(anyString()); + + ContentSession session = mock(ContentSession.class); + doReturn(root).when(session).getLatestRoot(); + + ContentRemoteSession remoteSession = createSession(session); + assertNull(remoteSession.readBinaryId("id")); + } + + @Test + public void testReadBinary() { + Blob blob = mock(Blob.class); + + ContentRemoteBinaryId binaryId = mock(ContentRemoteBinaryId.class); + doReturn(blob).when(binaryId).asBlob(); + + assertNotNull(createSession().readBinary(binaryId, new RemoteBinaryFilters())); + } + + @Test(expected = IllegalArgumentException.class) + public void testReadBinaryWithNullId() { + createSession().readBinary(null, new RemoteBinaryFilters()); + } + + @Test(expected = IllegalArgumentException.class) + public void testReadBinaryWithInvalidId() { + createSession().readBinary(mock(RemoteBinaryId.class), new RemoteBinaryFilters()); + } + + @Test(expected = IllegalArgumentException.class) + public void testReadBinaryWithNullFilters() { + createSession().readBinary(mock(ContentRemoteBinaryId.class), null); + } + + @Test + public void testWriteBinary() throws Exception { + Blob blob = mock(Blob.class); + + InputStream stream = mock(InputStream.class); + + Root root = mock(Root.class); + doReturn(blob).when(root).createBlob(stream); + + ContentSession session = mock(ContentSession.class); + doReturn(root).when(session).getLatestRoot(); + + ContentRemoteBinaries binaries = mock(ContentRemoteBinaries.class); + doReturn("id").when(binaries).put(blob); + + ContentRemoteSession remoteSession = createSession(session, binaries); + assertEquals("id", remoteSession.writeBinary(stream).asString()); + } + + @Test(expected = IllegalArgumentException.class) + public void testWriteBinaryWithNullStream() throws Exception { + createSession().writeBinary(null); + } + + @Test(expected = RuntimeException.class) + public void testWriteBinaryFailure() throws Exception { + InputStream stream = mock(InputStream.class); + + Root root = mock(Root.class); + doThrow(IOException.class).when(root).createBlob(stream); + + ContentSession session = mock(ContentSession.class); + doReturn(root).when(session).getLatestRoot(); + + ContentRemoteSession remoteSession = createSession(session); + remoteSession.writeBinary(stream); + } + + @Test + public void testReadBinaryLength() throws Exception { + Blob blob = mock(Blob.class); + doReturn(42L).when(blob).length(); + + ContentRemoteBinaryId binaryId = mock(ContentRemoteBinaryId.class); + doReturn(blob).when(binaryId).asBlob(); + + assertEquals(42L, createSession().readBinaryLength(binaryId)); + } + + @Test(expected = IllegalArgumentException.class) + public void testReadBinaryLengthWithNullBinaryId() throws Exception { + createSession().readBinaryLength(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testReadBinaryLengthWithInvalidBinaryId() throws Exception { + createSession().readBinaryLength(mock(RemoteBinaryId.class)); + } + +} diff --git oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteTreeTest.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteTreeTest.java new file mode 100644 index 0000000..027fbec --- /dev/null +++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/ContentRemoteTreeTest.java @@ -0,0 +1,947 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import com.google.common.collect.Sets; +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.remote.RemoteTree; +import org.apache.jackrabbit.oak.remote.RemoteTreeFilters; +import org.apache.jackrabbit.oak.remote.RemoteValue; +import org.apache.jackrabbit.util.ISO8601; +import org.junit.Test; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.Calendar; +import java.util.Map; +import java.util.Set; + +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.collect.Sets.newHashSet; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public class ContentRemoteTreeTest { + + private ContentRemoteTree createTree(Tree tree) { + return new ContentRemoteTree(tree, 0, new RemoteTreeFilters(), mock(ContentRemoteBinaries.class)); + } + + private ContentRemoteTree createTree(Tree tree, ContentRemoteBinaries binaries) { + return new ContentRemoteTree(tree, 0, new RemoteTreeFilters(), binaries); + } + + private ContentRemoteTree createTree(Tree tree, RemoteTreeFilters filters) { + return new ContentRemoteTree(tree, 0, filters, mock(ContentRemoteBinaries.class)); + } + + @Test + public void testGetBinaryProperty() { + InputStream stream = mock(InputStream.class); + + Blob blob = mock(Blob.class); + doReturn(stream).when(blob).getNewStream(); + + PropertyState property = mock(PropertyState.class); + doReturn("name").when(property).getName(); + doReturn(Type.BINARY).when(property).getType(); + doReturn(blob).when(property).getValue(Type.BINARY); + + Tree tree = mock(Tree.class); + doReturn(singletonList(property)).when(tree).getProperties(); + + ContentRemoteTree remoteTree = createTree(tree, new RemoteTreeFilters() { + + @Override + public long getBinaryThreshold() { + return Long.MAX_VALUE; + } + + }); + + Map properties = remoteTree.getProperties(); + + assertTrue(properties.containsKey("name")); + assertTrue(properties.get("name").isBinary()); + assertEquals(stream, properties.get("name").asBinary().get()); + } + + @Test + public void testGetMultiBinaryProperty() { + InputStream stream = mock(InputStream.class); + + Blob blob = mock(Blob.class); + doReturn(stream).when(blob).getNewStream(); + + PropertyState property = mock(PropertyState.class); + doReturn("name").when(property).getName(); + doReturn(Type.BINARIES).when(property).getType(); + doReturn(singletonList(blob)).when(property).getValue(Type.BINARIES); + + Tree tree = mock(Tree.class); + doReturn(singletonList(property)).when(tree).getProperties(); + + ContentRemoteTree remoteTree = createTree(tree, new RemoteTreeFilters() { + + @Override + public long getBinaryThreshold() { + return Long.MAX_VALUE; + } + + }); + + Map properties = remoteTree.getProperties(); + + assertTrue(properties.containsKey("name")); + assertTrue(properties.get("name").isMultiBinary()); + assertEquals(stream, getOnlyElement(properties.get("name").asMultiBinary()).get()); + } + + @Test + public void testGetBinaryIdProperty() { + Blob blob = mock(Blob.class); + + PropertyState property = mock(PropertyState.class); + doReturn("name").when(property).getName(); + doReturn(Type.BINARY).when(property).getType(); + doReturn(blob).when(property).getValue(Type.BINARY); + + Tree tree = mock(Tree.class); + doReturn(singletonList(property)).when(tree).getProperties(); + + ContentRemoteBinaries binaries = mock(ContentRemoteBinaries.class); + doReturn("id").when(binaries).put(blob); + + Map properties = createTree(tree, binaries).getProperties(); + + assertTrue(properties.containsKey("name")); + assertTrue(properties.get("name").isBinaryId()); + assertEquals("id", properties.get("name").asBinaryId()); + } + + @Test + public void testGetMultiBinaryIdProperty() { + Blob blob = mock(Blob.class); + + PropertyState property = mock(PropertyState.class); + doReturn("name").when(property).getName(); + doReturn(Type.BINARIES).when(property).getType(); + doReturn(singletonList(blob)).when(property).getValue(Type.BINARIES); + + Tree tree = mock(Tree.class); + doReturn(singletonList(property)).when(tree).getProperties(); + + ContentRemoteBinaries binaries = mock(ContentRemoteBinaries.class); + doReturn("id").when(binaries).put(blob); + + Map properties = createTree(tree, binaries).getProperties(); + + assertTrue(properties.containsKey("name")); + assertTrue(properties.get("name").isMultiBinaryId()); + assertEquals("id", getOnlyElement(properties.get("name").asMultiBinaryId())); + } + + @Test + public void testGetBooleanProperty() { + PropertyState property = mock(PropertyState.class); + doReturn("name").when(property).getName(); + doReturn(Type.BOOLEAN).when(property).getType(); + doReturn(true).when(property).getValue(Type.BOOLEAN); + + Tree tree = mock(Tree.class); + doReturn(singletonList(property)).when(tree).getProperties(); + + Map properties = createTree(tree).getProperties(); + + assertTrue(properties.containsKey("name")); + assertTrue(properties.get("name").isBoolean()); + assertEquals(true, properties.get("name").asBoolean()); + } + + @Test + public void testGetMultiBooleanProperty() { + PropertyState property = mock(PropertyState.class); + doReturn("name").when(property).getName(); + doReturn(Type.BOOLEANS).when(property).getType(); + doReturn(singletonList(true)).when(property).getValue(Type.BOOLEANS); + + Tree tree = mock(Tree.class); + doReturn(singletonList(property)).when(tree).getProperties(); + + Map properties = createTree(tree).getProperties(); + + assertTrue(properties.containsKey("name")); + assertTrue(properties.get("name").isMultiBoolean()); + assertEquals(true, getOnlyElement(properties.get("name").asMultiBoolean())); + } + + @Test + public void testGetDateProperty() { + Calendar calendar = Calendar.getInstance(); + + PropertyState property = mock(PropertyState.class); + doReturn("name").when(property).getName(); + doReturn(Type.DATE).when(property).getType(); + doReturn(ISO8601.format(calendar)).when(property).getValue(Type.DATE); + + Tree tree = mock(Tree.class); + doReturn(singletonList(property)).when(tree).getProperties(); + + Map properties = createTree(tree).getProperties(); + + assertTrue(properties.containsKey("name")); + assertTrue(properties.get("name").isDate()); + assertEquals(calendar.getTimeInMillis(), properties.get("name").asDate().longValue()); + } + + @Test + public void testGetMultiDateProperty() { + Calendar calendar = Calendar.getInstance(); + + PropertyState property = mock(PropertyState.class); + doReturn("name").when(property).getName(); + doReturn(Type.DATES).when(property).getType(); + doReturn(singletonList(ISO8601.format(calendar))).when(property).getValue(Type.DATES); + + Tree tree = mock(Tree.class); + doReturn(singletonList(property)).when(tree).getProperties(); + + Map properties = createTree(tree).getProperties(); + + assertTrue(properties.containsKey("name")); + assertTrue(properties.get("name").isMultiDate()); + assertEquals(calendar.getTimeInMillis(), getOnlyElement(properties.get("name").asMultiDate()).longValue()); + } + + @Test + public void testDecimalProperty() { + PropertyState property = mock(PropertyState.class); + doReturn("name").when(property).getName(); + doReturn(Type.DECIMAL).when(property).getType(); + doReturn(BigDecimal.ONE).when(property).getValue(Type.DECIMAL); + + Tree tree = mock(Tree.class); + doReturn(singletonList(property)).when(tree).getProperties(); + + Map properties = createTree(tree).getProperties(); + + assertTrue(properties.containsKey("name")); + assertTrue(properties.get("name").isDecimal()); + assertEquals(BigDecimal.ONE, properties.get("name").asDecimal()); + } + + @Test + public void testGetMultiDecimalProperty() { + PropertyState property = mock(PropertyState.class); + doReturn("name").when(property).getName(); + doReturn(Type.DECIMALS).when(property).getType(); + doReturn(singletonList(BigDecimal.ONE)).when(property).getValue(Type.DECIMALS); + + Tree tree = mock(Tree.class); + doReturn(singletonList(property)).when(tree).getProperties(); + + Map properties = createTree(tree).getProperties(); + + assertTrue(properties.containsKey("name")); + assertTrue(properties.get("name").isMultiDecimal()); + assertEquals(BigDecimal.ONE, getOnlyElement(properties.get("name").asMultiDecimal())); + } + + @Test + public void testDoubleProperty() { + PropertyState property = mock(PropertyState.class); + doReturn("name").when(property).getName(); + doReturn(Type.DOUBLE).when(property).getType(); + doReturn(4.2).when(property).getValue(Type.DOUBLE); + + Tree tree = mock(Tree.class); + doReturn(singletonList(property)).when(tree).getProperties(); + + Map properties = createTree(tree).getProperties(); + + assertTrue(properties.containsKey("name")); + assertTrue(properties.get("name").isDouble()); + assertEquals(4.2, properties.get("name").asDouble(), 1e-9); + } + + @Test + public void testGetMultiDoubleProperty() { + PropertyState property = mock(PropertyState.class); + doReturn("name").when(property).getName(); + doReturn(Type.DOUBLES).when(property).getType(); + doReturn(singletonList(4.2)).when(property).getValue(Type.DOUBLES); + + Tree tree = mock(Tree.class); + doReturn(singletonList(property)).when(tree).getProperties(); + + Map properties = createTree(tree).getProperties(); + + assertTrue(properties.containsKey("name")); + assertTrue(properties.get("name").isMultiDouble()); + assertEquals(4.2, getOnlyElement(properties.get("name").asMultiDouble()), 1e-9); + } + + @Test + public void testLongProperty() { + PropertyState property = mock(PropertyState.class); + doReturn("name").when(property).getName(); + doReturn(Type.LONG).when(property).getType(); + doReturn(42L).when(property).getValue(Type.LONG); + + Tree tree = mock(Tree.class); + doReturn(singletonList(property)).when(tree).getProperties(); + + Map properties = createTree(tree).getProperties(); + + assertTrue(properties.containsKey("name")); + assertTrue(properties.get("name").isLong()); + assertEquals(42L, properties.get("name").asLong().longValue()); + } + + @Test + public void testGetMultiLongProperty() { + PropertyState property = mock(PropertyState.class); + doReturn("name").when(property).getName(); + doReturn(Type.LONGS).when(property).getType(); + doReturn(singletonList(42L)).when(property).getValue(Type.LONGS); + + Tree tree = mock(Tree.class); + doReturn(singletonList(property)).when(tree).getProperties(); + + Map properties = createTree(tree).getProperties(); + + assertTrue(properties.containsKey("name")); + assertTrue(properties.get("name").isMultiLong()); + assertEquals(42L, getOnlyElement(properties.get("name").asMultiLong()).longValue()); + } + + @Test + public void testNameProperty() { + PropertyState property = mock(PropertyState.class); + doReturn("name").when(property).getName(); + doReturn(Type.NAME).when(property).getType(); + doReturn("value").when(property).getValue(Type.NAME); + + Tree tree = mock(Tree.class); + doReturn(singletonList(property)).when(tree).getProperties(); + + Map properties = createTree(tree).getProperties(); + + assertTrue(properties.containsKey("name")); + assertTrue(properties.get("name").isName()); + assertEquals("value", properties.get("name").asName()); + } + + @Test + public void testGetMultiNameProperty() { + PropertyState property = mock(PropertyState.class); + doReturn("name").when(property).getName(); + doReturn(Type.NAMES).when(property).getType(); + doReturn(singletonList("value")).when(property).getValue(Type.NAMES); + + Tree tree = mock(Tree.class); + doReturn(singletonList(property)).when(tree).getProperties(); + + Map properties = createTree(tree).getProperties(); + + assertTrue(properties.containsKey("name")); + assertTrue(properties.get("name").isMultiName()); + assertEquals("value", getOnlyElement(properties.get("name").asMultiName())); + } + + @Test + public void testPathProperty() { + PropertyState property = mock(PropertyState.class); + doReturn("name").when(property).getName(); + doReturn(Type.PATH).when(property).getType(); + doReturn("value").when(property).getValue(Type.PATH); + + Tree tree = mock(Tree.class); + doReturn(singletonList(property)).when(tree).getProperties(); + + Map 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..3b8b068 --- /dev/null +++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/CopyContentRemoteOperationTest.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.remote.RemoteCommitException; +import org.junit.Test; + +import static 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() throws Exception { + 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 = RemoteCommitException.class) + public void testCopyWithNonExistingSource() throws Exception { + 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 = RemoteCommitException.class) + public void testCopyWithExistingTarget() throws Exception { + 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 = RemoteCommitException.class) + public void testCopyWithNonExistingTargetParent() throws Exception { + 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..7eaf75a --- /dev/null +++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/MoveContentRemoteOperationTest.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.remote.RemoteCommitException; +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() throws Exception { + Root root = mock(Root.class); + doReturn(true).when(root).move("/source", "/target"); + + createOperation("/source", "/target").apply(root); + } + + @Test(expected = RemoteCommitException.class) + public void testMoveUnsuccessful() throws Exception { + 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..2873ff1 --- /dev/null +++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/RemoveContentRemoteOperationTest.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.remote.RemoteCommitException; +import org.junit.Test; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public class RemoveContentRemoteOperationTest { + + private RemoveContentRemoteOperation createOperation(String path) { + return new RemoveContentRemoteOperation(path); + } + + @Test + public void testRemove() throws Exception { + 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 = RemoteCommitException.class) + public void testRemoveWithNonExistingTree() throws Exception { + 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 = RemoteCommitException.class) + public void testRemoveWithNonRemovableTree() throws Exception { + 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..7f19182 --- /dev/null +++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/SetContentRemoteOperationTest.java @@ -0,0 +1,535 @@ +/* + * 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.RemoteCommitException; +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(mock(ContentRemoteBinaries.class), path, name, value); + } + + private SetContentRemoteOperation createOperation(ContentRemoteBinaries binaries, String path, String name, RemoteValue value) { + return new SetContentRemoteOperation(binaries, 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 = RemoteCommitException.class) + public void testSetWithNonExistingTree() throws Exception { + 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() throws Exception { + 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"); + + ContentRemoteBinaries binaries = mock(ContentRemoteBinaries.class); + doReturn(blob).when(binaries).get("id"); + + createOperation(binaries, "/test", "name", toBinaryId("id")).apply(root); + + verify(tree).setProperty("name", blob, Type.BINARY); + } + + @Test + public void setMultiBinaryIdProperty() throws Exception { + 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"); + + ContentRemoteBinaries binaries = mock(ContentRemoteBinaries.class); + doReturn(blob).when(binaries).get("id"); + + createOperation(binaries, "/test", "name", toMultiBinaryId(singletonList("id"))).apply(root); + + verify(tree).setProperty(eq("name"), argThat(isIterableReferencing(blob)), eq(Type.BINARIES)); + } + + @Test + public void testSetBooleanProperty() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "name", toBoolean(true)).apply(root); + + verify(tree).setProperty("name", true, Type.BOOLEAN); + } + + @Test + public void testSetMultiBooleanProperty() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "name", toMultiBoolean(singletonList(true))).apply(root); + + verify(tree).setProperty(eq("name"), argThat(isIterableContaining(true)), eq(Type.BOOLEANS)); + } + + @Test + public void testSetDateProperty() throws Exception { + 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() throws Exception { + 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() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "name", toDecimal(BigDecimal.ONE)).apply(root); + + verify(tree).setProperty("name", BigDecimal.ONE, Type.DECIMAL); + } + + @Test + public void testSetMultiDecimalProperty() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "name", toMultiDecimal(singletonList(BigDecimal.ONE))).apply(root); + + verify(tree).setProperty(eq("name"), argThat(isIterableContaining(BigDecimal.ONE)), eq(Type.DECIMALS)); + } + + @Test + public void testSetDoubleProperty() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "name", toDouble(4.2)).apply(root); + + verify(tree).setProperty("name", 4.2, Type.DOUBLE); + } + + @Test + public void testSetMultiDoubleProperty() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "name", toMultiDouble(singletonList(4.2))).apply(root); + + verify(tree).setProperty(eq("name"), argThat(isIterableContaining(4.2)), eq(Type.DOUBLES)); + } + + @Test + public void testSetLongProperty() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "name", toLong(42L)).apply(root); + + verify(tree).setProperty("name", 42L, Type.LONG); + } + + @Test + public void testSetMultiLongProperty() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "name", toMultiLong(singletonList(42L))).apply(root); + + verify(tree).setProperty(eq("name"), argThat(isIterableContaining(42L)), eq(Type.LONGS)); + } + + @Test + public void testSetNameProperty() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "name", toName("value")).apply(root); + + verify(tree).setProperty("name", "value", Type.NAME); + } + + @Test + public void testSetMultiNameProperty() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "name", toMultiName(singletonList("value"))).apply(root); + + verify(tree).setProperty(eq("name"), argThat(isIterableContaining("value")), eq(Type.NAMES)); + } + + @Test + public void testSetPathProperty() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "name", toPath("value")).apply(root); + + verify(tree).setProperty("name", "value", Type.PATH); + } + + @Test + public void testSetMultiPathProperty() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "name", toMultiPath(singletonList("value"))).apply(root); + + verify(tree).setProperty(eq("name"), argThat(isIterableContaining("value")), eq(Type.PATHS)); + } + + @Test + public void testSetReferenceProperty() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "name", toReference("value")).apply(root); + + verify(tree).setProperty("name", "value", Type.REFERENCE); + } + + @Test + public void testSetMultiReferenceProperty() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "name", toMultiReference(singletonList("value"))).apply(root); + + verify(tree).setProperty(eq("name"), argThat(isIterableContaining("value")), eq(Type.REFERENCES)); + } + + @Test + public void testSetStringProperty() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "name", toText("value")).apply(root); + + verify(tree).setProperty("name", "value", Type.STRING); + } + + @Test + public void testSetMultiStringProperty() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "name", toMultiText(singletonList("value"))).apply(root); + + verify(tree).setProperty(eq("name"), argThat(isIterableContaining("value")), eq(Type.STRINGS)); + } + + @Test + public void testSetUriProperty() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "name", toUri("value")).apply(root); + + verify(tree).setProperty("name", "value", Type.URI); + } + + @Test + public void testSetMultiUriProperty() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "name", toMultiUri(singletonList("value"))).apply(root); + + verify(tree).setProperty(eq("name"), argThat(isIterableContaining("value")), eq(Type.URIS)); + } + + @Test + public void testSetWeakReferenceProperty() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "name", toWeakReference("value")).apply(root); + + verify(tree).setProperty("name", "value", Type.WEAKREFERENCE); + } + + @Test + public void testSetMultiWeakReferenceProperty() throws Exception { + Tree tree = mock(Tree.class); + doReturn(true).when(tree).exists(); + + Root root = mock(Root.class); + doReturn(tree).when(root).getTree("/test"); + + createOperation("/test", "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..2f08017 --- /dev/null +++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/content/UnsetContentRemoteOperationTest.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.content; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.remote.RemoteCommitException; +import org.junit.Test; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class UnsetContentRemoteOperationTest { + + private UnsetContentRemoteOperation createOperation(String path, String name) { + return new UnsetContentRemoteOperation(path, name); + } + + @Test + public void testUnset() throws Exception { + 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 = RemoteCommitException.class) + public void testUnsetWithNonExistingTree() throws Exception { + 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 = RemoteCommitException.class) + public void testUnsetWithNonExistingProperty() throws Exception { + 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/RemoteServer.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/handler/RemoteServer.java new file mode 100644 index 0000000..60b0b30 --- /dev/null +++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/handler/RemoteServer.java @@ -0,0 +1,65 @@ +/* + * 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.http.RemoteServlet; +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/test/java/org/apache/jackrabbit/oak/remote/http/handler/RemoteServerIT.java oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/handler/RemoteServerIT.java new file mode 100644 index 0000000..a68265e --- /dev/null +++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/handler/RemoteServerIT.java @@ -0,0 +1,1361 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.remote.http.handler; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import com.mashape.unirest.http.HttpResponse; +import com.mashape.unirest.http.JsonNode; +import org.apache.jackrabbit.oak.NodeStoreFixture; +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.OakBaseTest; +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.api.Tree; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.jcr.Jcr; +import org.apache.jackrabbit.oak.remote.RemoteRepository; +import org.apache.jackrabbit.oak.remote.content.ContentRemoteRepository; +import org.apache.jackrabbit.util.ISO8601; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import javax.jcr.SimpleCredentials; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.Calendar; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.google.common.collect.Lists.newArrayList; +import static com.mashape.unirest.http.Unirest.get; +import static com.mashape.unirest.http.Unirest.head; +import static com.mashape.unirest.http.Unirest.patch; +import static com.mashape.unirest.http.Unirest.post; +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class RemoteServerIT extends OakBaseTest { + + private ContentRepository contentRepository; + + private ContentSession contentSession; + + private RemoteRepository remoteRepository; + + private RemoteServer remoteServer; + + public RemoteServerIT(NodeStoreFixture fixture) { + super(fixture); + } + + private InputStream asStream(String s) { + return new ByteArrayInputStream(s.getBytes()); + } + + private String asString(Calendar calendar) { + return ISO8601.format(calendar); + } + + private RemoteServer getRemoteServer(RemoteRepository repository, String host, int port) { + return new RemoteServer(repository, host, port); + } + + private RemoteRepository getRemoteRepository(ContentRepository repository) { + return new ContentRemoteRepository(repository); + } + + private ContentRepository getContentRepository() { + Oak oak = new Oak(store); + + new Jcr(oak); + + return oak.createContentRepository(); + } + + private ContentSession getContentSession(ContentRepository repository) throws Exception { + return repository.login(new SimpleCredentials("admin", "admin".toCharArray()), null); + } + + private String getHost() { + return "localhost"; + } + + private int getPort() { + return 28080; + } + + private String resource(String path) { + return "http://" + getHost() + ":" + getPort() + path; + } + + private String load(String name) throws Exception { + return Files.toString(new File(getClass().getResource(name).getFile()), Charsets.UTF_8); + } + + @Before + public void setUp() throws Exception { + contentRepository = getContentRepository(); + contentSession = getContentSession(contentRepository); + remoteRepository = getRemoteRepository(contentRepository); + remoteServer = getRemoteServer(remoteRepository, getHost(), getPort()); + remoteServer.start(); + } + + @After + public void tearDown() throws Exception { + remoteServer.stop(); + contentSession.close(); + } + + @Test + public void testReadLastRevision() throws Exception { + HttpResponse response = get(resource("/revisions/last")).basicAuth("admin", "admin").asJson(); + assertEquals(200, response.getStatus()); + + JSONObject payload = response.getBody().getObject(); + assertNotNull(payload.getString("revision")); + } + + @Test + public void testReadLastRevisionWithoutAuthentication() throws Exception { + assertEquals(401, get(resource("/revisions/last")).asJson().getStatus()); + } + + @Test + public void testReadLastRevisionTree() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + + root.commit(); + + HttpResponse response = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson(); + assertEquals(200, response.getStatus()); + } + + @Test + public void testReadLastRevisionTreeWithoutAuthentication() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + + root.commit(); + + HttpResponse response = get(resource("/revisions/last/tree/node")).asJson(); + assertEquals(401, response.getStatus()); + } + + @Test + public void testReadLastRevisionTreeRevision() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + + root.commit(); + + HttpResponse response = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson(); + assertFalse(response.getHeaders().getFirst("oak-revision").isEmpty()); + } + + @Test + public void testReadLastRevisionTreeWithNotExistingTree() throws Exception { + assertEquals(404, get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getStatus()); + } + + @Test + public void testReadLastRevisionTreeHasMoreChildren() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertFalse(body.getBoolean("hasMoreChildren")); + } + + @Test + public void testReadLastRevisionTreeChildren() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + + Tree child = node.addChild("child"); + child.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + } + + @Test + public void testReadLastRevisionTreeStringProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", "a", Type.STRING); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("string", property.getString("type")); + assertEquals("a", property.getString("value")); + } + + @Test + public void testReadLastRevisionTreeMultiStringProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", asList("a", "b"), Type.STRINGS); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("strings", property.getString("type")); + assertEquals("a", property.getJSONArray("value").getString(0)); + assertEquals("b", property.getJSONArray("value").getString(1)); + } + + @Test + public void testReadLastRevisionTreeBinaryProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", asList(root.createBlob(asStream("a")), root.createBlob(asStream("b"))), Type.BINARIES); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("binaryIds", property.getString("type")); + assertFalse(property.getJSONArray("value").getString(0).isEmpty()); + assertFalse(property.getJSONArray("value").getString(1).isEmpty()); + } + + @Test + public void testReadLastRevisionTreeMultiBinaryProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", root.createBlob(asStream("a")), Type.BINARY); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("binaryId", property.getString("type")); + assertFalse(property.getString("value").isEmpty()); + } + + @Test + public void testReadLastRevisionTreeLongProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", 42L, Type.LONG); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("long", property.getString("type")); + assertEquals(42L, property.getLong("value")); + } + + @Test + public void testReadLastRevisionTreeMultiLongProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", asList(4L, 2L), Type.LONGS); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("longs", property.getString("type")); + assertEquals(4L, property.getJSONArray("value").getLong(0)); + assertEquals(2L, property.getJSONArray("value").getLong(1)); + } + + @Test + public void testReadLastRevisionTreeDoubleProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", 4.2, Type.DOUBLE); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("double", property.getString("type")); + assertEquals(4.2, property.getDouble("value"), 1e-10); + } + + @Test + public void testReadLastRevisionTreeMultiDoubleProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", asList(4.2, 2.4), Type.DOUBLES); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("doubles", property.getString("type")); + assertEquals(4.2, property.getJSONArray("value").getDouble(0), 1e-10); + assertEquals(2.4, property.getJSONArray("value").getDouble(1), 1e-10); + } + + @Test + public void testReadLastRevisionTreeDateProperty() throws Exception { + Calendar calendar = Calendar.getInstance(); + + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", asString(calendar), Type.DATE); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("date", property.getString("type")); + assertEquals(calendar.getTimeInMillis(), property.getLong("value")); + } + + @Test + public void testReadLastRevisionTreeMultiDateProperty() throws Exception { + Calendar calendar = Calendar.getInstance(); + + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", asList(asString(calendar), asString(calendar)), Type.DATES); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("dates", property.getString("type")); + assertEquals(calendar.getTimeInMillis(), property.getJSONArray("value").getLong(0)); + assertEquals(calendar.getTimeInMillis(), property.getJSONArray("value").getLong(1)); + } + + @Test + public void testReadLastRevisionTreeBooleanProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", true, Type.BOOLEAN); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("boolean", property.getString("type")); + assertEquals(true, property.getBoolean("value")); + } + + @Test + public void testReadLastRevisionTreeMultiBooleanProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", asList(true, false), Type.BOOLEANS); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("booleans", property.getString("type")); + assertEquals(true, property.getJSONArray("value").getBoolean(0)); + assertEquals(false, property.getJSONArray("value").getBoolean(1)); + } + + @Test + public void testReadLastRevisionTreeNameProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", "value", Type.NAME); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("name", property.getString("type")); + assertEquals("value", property.getString("value")); + } + + @Test + public void testReadLastRevisionTreeMultiNameProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", asList("first", "second"), Type.NAMES); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("names", property.getString("type")); + assertEquals("first", property.getJSONArray("value").getString(0)); + assertEquals("second", property.getJSONArray("value").getString(1)); + } + + @Test + public void testReadLastRevisionTreePathProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", "/value", Type.PATH); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("path", property.getString("type")); + assertEquals("/value", property.getString("value")); + } + + @Test + public void testReadLastRevisionTreeMultiPathProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", asList("/first", "/second"), Type.PATHS); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("paths", property.getString("type")); + assertEquals("/first", property.getJSONArray("value").getString(0)); + assertEquals("/second", property.getJSONArray("value").getString(1)); + } + + @Test + public void testReadLastRevisionTreeReferenceProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", "value", Type.REFERENCE); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("reference", property.getString("type")); + assertEquals("value", property.getString("value")); + } + + @Test + public void testReadLastRevisionTreeMultiReferenceProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", asList("first", "second"), Type.REFERENCES); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("references", property.getString("type")); + assertEquals("first", property.getJSONArray("value").getString(0)); + assertEquals("second", property.getJSONArray("value").getString(1)); + } + + @Test + public void testReadLastRevisionTreeWeakReferenceProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", "value", Type.WEAKREFERENCE); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("weakReference", property.getString("type")); + assertEquals("value", property.getString("value")); + } + + @Test + public void testReadLastRevisionTreeMultiWeakReferenceProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", asList("first", "second"), Type.WEAKREFERENCES); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("weakReferences", property.getString("type")); + assertEquals("first", property.getJSONArray("value").getString(0)); + assertEquals("second", property.getJSONArray("value").getString(1)); + } + + @Test + public void testReadLastRevisionTreeUriProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", "http://acme.org", Type.URI); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("uri", property.getString("type")); + assertEquals("http://acme.org", property.getString("value")); + } + + @Test + public void testReadLastRevisionTreeMultiUriProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", asList("http://acme.org", "http://acme.com"), Type.URIS); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("uris", property.getString("type")); + assertEquals("http://acme.org", property.getJSONArray("value").getString(0)); + assertEquals("http://acme.com", property.getJSONArray("value").getString(1)); + } + + @Test + public void testReadLastRevisionTreeDecimalProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", BigDecimal.ZERO, Type.DECIMAL); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("decimal", property.getString("type")); + assertEquals("0", property.getString("value")); + } + + @Test + public void testReadLastRevisionTreeMultiDecimalProperty() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("property", asList(BigDecimal.ZERO, BigDecimal.ONE), Type.DECIMALS); + + root.commit(); + + JSONObject body = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getBody().getObject(); + assertTrue(body.getJSONObject("children").isNull("child")); + + JSONObject property = body.getJSONObject("properties").getJSONObject("property"); + assertEquals("decimals", property.getString("type")); + assertEquals("0", property.getJSONArray("value").getString(0)); + assertEquals("1", property.getJSONArray("value").getString(1)); + } + + @Test + public void testReadLastRevisionTreeWithDepth() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + + Tree child = node.addChild("child"); + child.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + + Tree grandChild = child.addChild("grandChild"); + grandChild.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + + root.commit(); + + HttpResponse response = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").queryString("depth", 1).asJson(); + assertEquals(200, response.getStatus()); + + JSONObject body = response.getBody().getObject(); + assertFalse(body.getJSONObject("children").isNull("child")); + assertTrue(body.getJSONObject("children").getJSONObject("child").getJSONObject("children").isNull("grandChild")); + } + + @Test + public void testReadLastRevisionTreeWithWithPropertyFilters() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("foo", "foo", Type.STRING); + node.setProperty("bar", "bar", Type.STRING); + node.setProperty("baz", "baz", Type.STRING); + + root.commit(); + + HttpResponse response = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").queryString("properties", "ba*").queryString("properties", "-baz").asJson(); + assertEquals(200, response.getStatus()); + + JSONObject body = response.getBody().getObject(); + assertFalse(body.getJSONObject("properties").has("foo")); + assertTrue(body.getJSONObject("properties").has("bar")); + assertFalse(body.getJSONObject("properties").has("baz")); + } + + @Test + public void testReadLastRevisionTreeWithWithNodeFilters() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + + Tree foo = node.addChild("foo"); + foo.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + + Tree bar = node.addChild("bar"); + bar.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + + Tree baz = node.addChild("baz"); + baz.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + + root.commit(); + + HttpResponse response = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").queryString("children", "ba*").queryString("children", "-baz").asJson(); + assertEquals(200, response.getStatus()); + + JSONObject body = response.getBody().getObject(); + assertFalse(body.getJSONObject("children").has("foo")); + assertTrue(body.getJSONObject("children").has("bar")); + assertFalse(body.getJSONObject("children").has("baz")); + } + + @Test + public void testReadTreeAtRevision() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + + root.commit(); + + String revision = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson().getHeaders().getFirst("oak-revision"); + + HttpResponse response = get(resource("/revisions/{revision}/tree/node")).basicAuth("admin", "admin").routeParam("revision", revision).asJson(); + assertEquals(200, response.getStatus()); + assertEquals(revision, response.getHeaders().getFirst("oak-revision")); + } + + @Test + public void testReadTreeAtRevisionWithInvalidRevision() throws Exception { + assertEquals(410, get(resource("/revisions/any/tree/node")).basicAuth("admin", "admin").asJson().getStatus()); + } + + @Test + public void testReadBinary() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("binary", root.createBlob(asStream("test")), Type.BINARY); + + root.commit(); + + HttpResponse response = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson(); + String binaryId = response.getBody().getObject().getJSONObject("properties").getJSONObject("binary").getString("value"); + + HttpResponse binaryResponse = get(resource("/binaries/{binaryId}")).basicAuth("admin", "admin").routeParam("binaryId", binaryId).asString(); + assertEquals(200, binaryResponse.getStatus()); + assertEquals("test", binaryResponse.getBody()); + assertEquals("4", binaryResponse.getHeaders().getFirst("content-length")); + } + + @Test + public void testReadBinaryWithoutAuthentication() throws Exception { + HttpResponse response = get(resource("/binaries/any")).asString(); + assertEquals(401, response.getStatus()); + } + + @Test + public void testReadBinaryWithInvalidId() throws Exception { + HttpResponse response = get(resource("/binaries/any")).basicAuth("admin", "admin").asString(); + assertEquals(404, response.getStatus()); + } + + @Test + public void testReadBinaryRangeOffsetOnly() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("binary", root.createBlob(asStream("0123456789")), Type.BINARY); + + root.commit(); + + HttpResponse response = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson(); + String binaryId = response.getBody().getObject().getJSONObject("properties").getJSONObject("binary").getString("value"); + + // Offset = 0 + HttpResponse binaryResponse = get(resource("/binaries/{binaryId}")) + .basicAuth("admin", "admin") + .routeParam("binaryId", binaryId) + .header("Range", "bytes=0") + .asString(); + assertEquals(206, binaryResponse.getStatus()); + assertTrue(binaryResponse.getHeaders().containsKey("content-range")); + assertTrue(binaryResponse.getHeaders().getFirst("content-range").matches("^\\s*0\\s*\\-\\s*9\\s*/\\s*(10|\\*)\\s*$")); + assertEquals("10", binaryResponse.getHeaders().getFirst("content-length")); + assertEquals("0123456789", binaryResponse.getBody()); + + // Offset = 3 + binaryResponse = get(resource("/binaries/{binaryId}")) + .basicAuth("admin", "admin") + .routeParam("binaryId", binaryId) + .header("Range", "bytes = 3") + .asString(); + assertEquals(206, binaryResponse.getStatus()); + assertTrue(binaryResponse.getHeaders().containsKey("content-range")); + assertTrue(binaryResponse.getHeaders().getFirst("content-range").matches("^\\s*3\\s*\\-\\s*9\\s*/\\s*(10|\\*)\\s*$")); + assertEquals("7", binaryResponse.getHeaders().getFirst("content-length")); + assertEquals("3456789", binaryResponse.getBody()); + + // Offset = 9 (last) + binaryResponse = get(resource("/binaries/{binaryId}")) + .basicAuth("admin", "admin") + .routeParam("binaryId", binaryId) + .header("Range", "bytes=9") + .asString(); + assertEquals(206, binaryResponse.getStatus()); + assertTrue(binaryResponse.getHeaders().containsKey("content-range")); + assertTrue(binaryResponse.getHeaders().getFirst("content-range").matches("^\\s*9\\s*\\-\\s*9\\s*/\\s*(10|\\*)\\s*$")); + assertEquals("1", binaryResponse.getHeaders().getFirst("content-length")); + assertEquals("9", binaryResponse.getBody()); + } + + @Test + public void testReadBinaryRangeSuffix() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("binary", root.createBlob(asStream("0123456789")), Type.BINARY); + + root.commit(); + + HttpResponse response = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson(); + String binaryId = response.getBody().getObject().getJSONObject("properties").getJSONObject("binary").getString("value"); + + // Last 10 bytes (full body) + HttpResponse binaryResponse = get(resource("/binaries/{binaryId}")) + .basicAuth("admin", "admin") + .routeParam("binaryId", binaryId) + .header("Range", "bytes=-10") + .asString(); + assertEquals(206, binaryResponse.getStatus()); + assertTrue(binaryResponse.getHeaders().containsKey("content-range")); + assertTrue(binaryResponse.getHeaders().getFirst("content-range").matches("^\\s*0\\s*\\-\\s*9\\s*/\\s*(10|\\*)\\s*$")); + assertEquals("10", binaryResponse.getHeaders().getFirst("content-length")); + assertEquals("0123456789", binaryResponse.getBody()); + + // Last 3 bytes + binaryResponse = get(resource("/binaries/{binaryId}")) + .basicAuth("admin", "admin") + .routeParam("binaryId", binaryId) + .header("Range", "bytes=-3") + .asString(); + assertEquals(206, binaryResponse.getStatus()); + assertTrue(binaryResponse.getHeaders().containsKey("content-range")); + assertTrue(binaryResponse.getHeaders().getFirst("content-range").matches("^\\s*7\\s*\\-\\s*9\\s*/\\s*(10|\\*)\\s*$")); + assertEquals("3", binaryResponse.getHeaders().getFirst("content-length")); + assertEquals("789", binaryResponse.getBody()); + + // Last 1 byte + binaryResponse = get(resource("/binaries/{binaryId}")) + .basicAuth("admin", "admin") + .routeParam("binaryId", binaryId) + .header("Range", "bytes=-1") + .asString(); + assertEquals(206, binaryResponse.getStatus()); + assertTrue(binaryResponse.getHeaders().containsKey("content-range")); + assertTrue(binaryResponse.getHeaders().getFirst("content-range").matches("^\\s*9\\s*\\-\\s*9\\s*/\\s*(10|\\*)\\s*$")); + assertEquals("1", binaryResponse.getHeaders().getFirst("content-length")); + assertEquals("9", binaryResponse.getBody()); + } + + @Test + public void testReadBinarySingleRange() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("binary", root.createBlob(asStream("0123456789")), Type.BINARY); + + root.commit(); + + HttpResponse response = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson(); + String binaryId = response.getBody().getObject().getJSONObject("properties").getJSONObject("binary").getString("value"); + + // Range 0-9 (full body) + HttpResponse binaryResponse = get(resource("/binaries/{binaryId}")) + .basicAuth("admin", "admin") + .routeParam("binaryId", binaryId) + .header("Range", "bytes=0-9") + .asString(); + assertEquals(206, binaryResponse.getStatus()); + assertTrue(binaryResponse.getHeaders().containsKey("content-range")); + assertTrue(binaryResponse.getHeaders().getFirst("content-range").matches("^\\s*0\\s*\\-\\s*9\\s*/\\s*(10|\\*)\\s*$")); + assertEquals("10", binaryResponse.getHeaders().getFirst("content-length")); + assertEquals("0123456789", binaryResponse.getBody()); + + // Range 0- (full body) + binaryResponse = get(resource("/binaries/{binaryId}")) + .basicAuth("admin", "admin") + .routeParam("binaryId", binaryId) + .header("Range", "bytes=0-") + .asString(); + assertEquals(206, binaryResponse.getStatus()); + assertTrue(binaryResponse.getHeaders().containsKey("content-range")); + assertTrue(binaryResponse.getHeaders().getFirst("content-range").matches("^\\s*0\\s*\\-\\s*9\\s*/\\s*(10|\\*)\\s*$")); + assertEquals("10", binaryResponse.getHeaders().getFirst("content-length")); + assertEquals("0123456789", binaryResponse.getBody()); + + // Range 3-6 + binaryResponse = get(resource("/binaries/{binaryId}")) + .basicAuth("admin", "admin") + .routeParam("binaryId", binaryId) + .header("Range", "bytes=3- 6") + .asString(); + assertEquals(206, binaryResponse.getStatus()); + assertTrue(binaryResponse.getHeaders().containsKey("content-range")); + assertTrue(binaryResponse.getHeaders().getFirst("content-range").matches("^\\s*3\\s*\\-\\s*6\\s*/\\s*(10|\\*)\\s*$")); + assertEquals("4", binaryResponse.getHeaders().getFirst("content-length")); + assertEquals("3456", binaryResponse.getBody()); + + // Range 9-9 + binaryResponse = get(resource("/binaries/{binaryId}")) + .basicAuth("admin", "admin") + .routeParam("binaryId", binaryId) + .header("Range", "bytes= 9 - 9") + .asString(); + assertEquals(206, binaryResponse.getStatus()); + assertTrue(binaryResponse.getHeaders().containsKey("content-range")); + assertTrue(binaryResponse.getHeaders().getFirst("content-range").matches("^\\s*9\\s*\\-\\s*9\\s*/\\s*(10|\\*)\\s*$")); + assertEquals("1", binaryResponse.getHeaders().getFirst("content-length")); + assertEquals("9", binaryResponse.getBody()); + } + + @Test + public void testReadBinaryMultipleRanges() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("binary", root.createBlob(asStream("0123456789")), Type.BINARY); + + root.commit(); + + HttpResponse response = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson(); + String binaryId = response.getBody().getObject().getJSONObject("properties").getJSONObject("binary").getString("value"); + + HttpResponse binaryResponse = get(resource("/binaries/{binaryId}")) + .basicAuth("admin", "admin") + .routeParam("binaryId", binaryId) + .header("Range", "bytes=0,-1 , 2-2, 3-6") + .asString(); + assertEquals(206, binaryResponse.getStatus()); + + String contentType = binaryResponse.getHeaders().getFirst("content-type"); + assertNotNull(contentType); + + Pattern multipartDelimiterPattern = Pattern.compile("^\\s*multipart/byteranges;\\s*boundary=(.+)"); + Matcher matcher = multipartDelimiterPattern.matcher(contentType); + assertTrue(matcher.matches()); + + //TODO: Validate response body + } + + @Test + public void testReadBinaryInvalidRanges() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("binary", root.createBlob(asStream("0123456789")), Type.BINARY); + + root.commit(); + + HttpResponse response = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson(); + String binaryId = response.getBody().getObject().getJSONObject("properties").getJSONObject("binary").getString("value"); + + // Unknown range unit = elephant + HttpResponse binaryResponse = get(resource("/binaries/{binaryId}")) + .basicAuth("admin", "admin") + .routeParam("binaryId", binaryId) + .header("Range", "elephant=0-9") + .asString(); + assertNotEquals(206, binaryResponse.getStatus()); + assertNotEquals(500, binaryResponse.getStatus()); + + // Invalid range header + binaryResponse = get(resource("/binaries/{binaryId}")) + .basicAuth("admin", "admin") + .routeParam("binaryId", binaryId) + .header("Range", "this is not correct") + .asString(); + assertNotEquals(206, binaryResponse.getStatus()); + assertNotEquals(500, binaryResponse.getStatus()); + + // Missing range unit + binaryResponse = get(resource("/binaries/{binaryId}")) + .basicAuth("admin", "admin") + .routeParam("binaryId", binaryId) + .header("Range", "0") + .asString(); + assertNotEquals(206, binaryResponse.getStatus()); + assertNotEquals(500, binaryResponse.getStatus()); + + // Range limit greater than file size + binaryResponse = get(resource("/binaries/{binaryId}")) + .basicAuth("admin", "admin") + .routeParam("binaryId", binaryId) + .header("Range", "bytes=0-1000") + .asString(); + assertNotEquals(206, binaryResponse.getStatus()); + assertNotEquals(500, binaryResponse.getStatus()); + + // Range start greater than end + binaryResponse = get(resource("/binaries/{binaryId}")) + .basicAuth("admin", "admin") + .routeParam("binaryId", binaryId) + .header("Range", "bytes=9-8") + .asString(); + assertNotEquals(206, binaryResponse.getStatus()); + assertNotEquals(500, binaryResponse.getStatus()); + + // One bad range will "break" all ranges + binaryResponse = get(resource("/binaries/{binaryId}")) + .basicAuth("admin", "admin") + .routeParam("binaryId", binaryId) + .header("Range", "bytes=0, -1, 10000") + .asString(); + assertNotEquals(206, binaryResponse.getStatus()); + assertNotEquals(500, binaryResponse.getStatus()); + } + + @Test + public void testExistsBinary() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree node = root.getTree("/").addChild("node"); + node.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + node.setProperty("binary", root.createBlob(asStream("0123456789")), Type.BINARY); + + root.commit(); + + HttpResponse response = get(resource("/revisions/last/tree/node")).basicAuth("admin", "admin").asJson(); + String binaryId = response.getBody().getObject().getJSONObject("properties").getJSONObject("binary").getString("value"); + + // Offset = 0 + HttpResponse binaryResponse = head(resource("/binaries/{binaryId}")) + .basicAuth("admin", "admin") + .routeParam("binaryId", binaryId) + .asString(); + assertEquals(200, binaryResponse.getStatus()); + assertEquals("bytes", binaryResponse.getHeaders().getFirst("accept-ranges")); + } + + @Test + public void testExistsBinaryWithInvalidId() throws Exception { + HttpResponse response = head(resource("/binaries/any")).basicAuth("admin", "admin").asString(); + assertEquals(404, response.getStatus()); + } + + @Test + public void testExistsBinaryWithoutAuthentication() throws Exception { + HttpResponse response = head(resource("/binaries/any")).asString(); + assertEquals(401, response.getStatus()); + } + + @Test + public void testCreateBinary() throws Exception { + HttpResponse response = post(resource("/binaries")).basicAuth("admin", "admin").body("body").asJson(); + assertEquals(201, response.getStatus()); + + String binaryId = response.getBody().getObject().getString("binaryId"); + assertFalse(binaryId.isEmpty()); + + HttpResponse binaryResponse = get(resource("/binaries/{binaryId}")).basicAuth("admin", "admin").routeParam("binaryId", binaryId).asString(); + assertEquals(200, binaryResponse.getStatus()); + assertEquals("body", binaryResponse.getBody()); + } + + @Test + public void testCreateBinaryWithoutAuthentication() throws Exception { + assertEquals(401, post(resource("/binaries")).body("body").asJson().getStatus()); + } + + @Test + public void testPatchLastRevisionAddNodeBinaryProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeBinaryProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddNodeMultiBinaryProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeMultiBinaryProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddNodeBooleanProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeBooleanProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddNodeMultiBooleanProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeMultiBooleanProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddNodeDateProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeDateProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddNodeMultiDateProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeMultiDateProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddDecimalProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeDecimalProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddMultiDecimalProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeMultiDecimalProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddDoubleProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeDoubleProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddMultiDoubleProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeMultiDoubleProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddLongProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeLongProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddMultiLongProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeMultiLongProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddNameProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeNameProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddMultiNameProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeMultiNameProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddPathProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodePathProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddMultiPathProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeMultiPathProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddReferenceProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeReferenceProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddMultiReferenceProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeMultiReferenceProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddStringProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeStringProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddMultiStringProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeMultiStringProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddUriProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeUriProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddMultiUriProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeMultiUriProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddWeakReferenceProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeWeakReferenceProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testPatchLastRevisionAddMultiWeakReferenceProperty() throws Exception { + HttpResponse response = patch(resource("/revisions/last/tree")).basicAuth("admin", "admin").body(load("addNodeMultiWeakReferenceProperty.json")).asJson(); + assertEquals(201, response.getStatus()); + } + + @Test + public void testSearchLastRevision() throws Exception { + Root root = contentSession.getLatestRoot(); + + Tree test = root.getTree("/").addChild("test"); + test.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + + Tree n1 = test.addChild("one"); + n1.setProperty("name", "one", Type.STRING); + n1.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + + Tree n2 = test.addChild("two"); + n2.setProperty("name", "two", Type.STRING); + n2.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME); + + root.commit(); + + HttpResponse response = get(resource("/revisions/last/tree")) + .basicAuth("admin", "admin") + .queryString("query", "select name from nt:unstructured as node where jcr:path like '/test/%'") + .queryString("language", "sql") + .queryString("offset", 0) + .queryString("limit", 10) + .asJson(); + + assertEquals(200, response.getStatus()); + + JSONObject results = response.getBody().getObject(); + assertNotNull(results.getLong("total")); + + List columns = getStringArray(results, "columns"); + assertTrue(columns.contains("name")); + assertTrue(columns.contains("jcr:path")); + + List selectors = getStringArray(results, "selectors"); + assertTrue(selectors.contains("node")); + } + + private List getStringArray(JSONObject parent, String name) { + List result = newArrayList(); + + JSONArray array = parent.getJSONArray(name); + + for (int i = 0; i < array.length(); i++) { + result.add(array.getString(i)); + } + + return result; + } + + @Test + public void testSearchLastRevisionWithoutAuthentication() throws Exception { + HttpResponse response = get(resource("/revisions/last/tree")) + .queryString("query", "select name from nt:unstructured as node where jcr:path like '/test/%'") + .queryString("language", "sql") + .queryString("offset", 0) + .queryString("limit", 10) + .asJson(); + + assertEquals(401, response.getStatus()); + } + + @Test + public void testSearchLastRevisionWithoutQuery() throws Exception { + HttpResponse response = get(resource("/revisions/last/tree")) + .basicAuth("admin", "admin") + .queryString("language", "sql") + .queryString("offset", 0) + .queryString("limit", 10) + .asJson(); + + assertEquals(400, response.getStatus()); + } + + @Test + public void testSearchLastRevisionWithoutLanguage() throws Exception { + HttpResponse response = get(resource("/revisions/last/tree")) + .basicAuth("admin", "admin") + .queryString("query", "select name from nt:unstructured as node where jcr:path like '/test/%'") + .queryString("offset", 0) + .queryString("limit", 10) + .asJson(); + + assertEquals(400, response.getStatus()); + } + + @Test + public void testSearchLastRevisionWithoutOffset() throws Exception { + HttpResponse response = get(resource("/revisions/last/tree")) + .basicAuth("admin", "admin") + .queryString("query", "select name from nt:unstructured as node where jcr:path like '/test/%'") + .queryString("language", "sql") + .queryString("limit", 10) + .asJson(); + + assertEquals(400, response.getStatus()); + } + + @Test + public void testSearchLastRevisionWithoutLimit() throws Exception { + HttpResponse response = get(resource("/revisions/last/tree")) + .basicAuth("admin", "admin") + .queryString("query", "select name from nt:unstructured as node where jcr:path like '/test/%'") + .queryString("language", "sql") + .queryString("offset", 0) + .asJson(); + + assertEquals(400, response.getStatus()); + } + +} 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..23872d5 --- /dev/null +++ oak-remote/src/test/java/org/apache/jackrabbit/oak/remote/http/matcher/MatchersTest.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.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 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-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeBinaryProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeBinaryProperty.json new file mode 100644 index 0000000..d629570 --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeBinaryProperty.json @@ -0,0 +1,16 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "binary", + "value": "dGVzdA==" + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeBooleanProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeBooleanProperty.json new file mode 100644 index 0000000..5113aff --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeBooleanProperty.json @@ -0,0 +1,16 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "boolean", + "value": true + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeDateProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeDateProperty.json new file mode 100644 index 0000000..cf9b491 --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeDateProperty.json @@ -0,0 +1,16 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "date", + "value": 1426849486095 + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeDecimalProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeDecimalProperty.json new file mode 100644 index 0000000..3f977a9 --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeDecimalProperty.json @@ -0,0 +1,16 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "decimal", + "value": "1.23e3" + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeDoubleProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeDoubleProperty.json new file mode 100644 index 0000000..642e882 --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeDoubleProperty.json @@ -0,0 +1,16 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "double", + "value": 1.23 + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeLongProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeLongProperty.json new file mode 100644 index 0000000..05fd973 --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeLongProperty.json @@ -0,0 +1,16 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "long", + "value": 123 + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiBinaryProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiBinaryProperty.json new file mode 100644 index 0000000..c923138 --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiBinaryProperty.json @@ -0,0 +1,19 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "binaries", + "value": [ + "dGVzdA==", + "dGVzdA==" + ] + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiBooleanProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiBooleanProperty.json new file mode 100644 index 0000000..34f9117 --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiBooleanProperty.json @@ -0,0 +1,19 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "booleans", + "value": [ + true, + false + ] + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiDateProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiDateProperty.json new file mode 100644 index 0000000..32e56be --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiDateProperty.json @@ -0,0 +1,19 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "dates", + "value": [ + 1426849486095, + 1426849486095 + ] + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiDecimalProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiDecimalProperty.json new file mode 100644 index 0000000..36425eb --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiDecimalProperty.json @@ -0,0 +1,19 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "decimals", + "value": [ + "1.23e3", + "4.56e4" + ] + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiDoubleProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiDoubleProperty.json new file mode 100644 index 0000000..d07a1e3 --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiDoubleProperty.json @@ -0,0 +1,19 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "doubles", + "value": [ + 1.23, + 4.56 + ] + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiLongProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiLongProperty.json new file mode 100644 index 0000000..59d543c --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiLongProperty.json @@ -0,0 +1,19 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "longs", + "value": [ + 123, + 456 + ] + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiNameProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiNameProperty.json new file mode 100644 index 0000000..c803983 --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiNameProperty.json @@ -0,0 +1,19 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "names", + "value": [ + "one", + "two" + ] + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiPathProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiPathProperty.json new file mode 100644 index 0000000..1a15fad --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiPathProperty.json @@ -0,0 +1,19 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "paths", + "value": [ + "/one", + "/two" + ] + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiReferenceProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiReferenceProperty.json new file mode 100644 index 0000000..5f20066 --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiReferenceProperty.json @@ -0,0 +1,19 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "references", + "value": [ + "one", + "two" + ] + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiStringProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiStringProperty.json new file mode 100644 index 0000000..582432b --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiStringProperty.json @@ -0,0 +1,19 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "strings", + "value": [ + "one", + "two" + ] + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiUriProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiUriProperty.json new file mode 100644 index 0000000..8cae506 --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiUriProperty.json @@ -0,0 +1,19 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "uris", + "value": [ + "http://www.acme.com", + "http://www.acme.org" + ] + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiWeakReferenceProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiWeakReferenceProperty.json new file mode 100644 index 0000000..e6fb60b --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeMultiWeakReferenceProperty.json @@ -0,0 +1,19 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "weakReferences", + "value": [ + "one", + "two" + ] + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeNameProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeNameProperty.json new file mode 100644 index 0000000..81afcb7 --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeNameProperty.json @@ -0,0 +1,16 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "name", + "value": "one" + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodePathProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodePathProperty.json new file mode 100644 index 0000000..4c3ab61 --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodePathProperty.json @@ -0,0 +1,16 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "path", + "value": "/one" + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeReferenceProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeReferenceProperty.json new file mode 100644 index 0000000..60dde7d --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeReferenceProperty.json @@ -0,0 +1,16 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "reference", + "value": "one" + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeStringProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeStringProperty.json new file mode 100644 index 0000000..3facac7 --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeStringProperty.json @@ -0,0 +1,16 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "string", + "value": "one" + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeUriProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeUriProperty.json new file mode 100644 index 0000000..1984fa3 --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeUriProperty.json @@ -0,0 +1,16 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "uri", + "value": "http://www.acme.com" + } + } + } +] \ No newline at end of file diff --git oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeWeakReferenceProperty.json oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeWeakReferenceProperty.json new file mode 100644 index 0000000..6311c6f --- /dev/null +++ oak-remote/src/test/resources/org/apache/jackrabbit/oak/remote/http/handler/addNodeWeakReferenceProperty.json @@ -0,0 +1,16 @@ +[ + { + "op": "add", + "path": "/test", + "properties": { + "jcr:primaryType": { + "type": "name", + "value": "nt:unstructured" + }, + "property": { + "type": "weakReference", + "value": "one" + } + } + } +] \ No newline at end of file diff --git oak-run/pom.xml oak-run/pom.xml index d51937f..e7c4f00 100644 --- oak-run/pom.xml +++ oak-run/pom.xml @@ -240,6 +240,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 7e0a2cf..786fcab 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 @@ -99,6 +99,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; @@ -1129,6 +1131,9 @@ public final 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 f073028..3abeceb 100644 --- pom.xml +++ pom.xml @@ -54,6 +54,7 @@ oak-it oak-pojosr oak-authorization-cug + oak-remote