diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..a3e764ae7b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.settings +.classpath +.project +target diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000000..c9baaba9066 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +script: mvn verify -PintegrationTesting +language: java +jdk: + - openjdk6 +notifications: + email: + - oak-dev@jackrabbit.apache.org diff --git a/README.md b/README.md new file mode 100644 index 00000000000..c086ae241db --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +======================================================= +Jackrabbit Oak - the next generation content repository +======================================================= + +Jackrabbit Oak is an effort to implement a scalable and performant +hierarchical content repository for use as the foundation of modern +world-class web sites and other demanding content applications. + +The Oak effort is a part of the Apache Jackrabbit project. +Apache Jackrabbit is a project of the Apache Software Foundation. + +Oak is currently alpha-level software. Use at your own risk with no +stability or compatibility guarantees. + +Getting Started +--------------- + +To get started with Oak, build the latest sources with +Maven 3 and Java 6 (or higher) like this: + + mvn clean install + +To enable all integration tests, including the JCR TCK, use: + + mvn clean install -PintegrationTesting + +Before committing changes or submitting a patch, please make sure that +the above integration testing build passes without errors. If you like, +you can enable integration tests by default by setting the +`OAK_INTEGRATION_TESTING` environment variable. + +The build consists of the following main components: + + - oak-parent - parent POM + - oak-commons - shared utility code + - oak-mk-api - MicroKernel API + - oak-mk - default MicroKernel implementation + - oak-mk-remote - MicroKernel remoting + - [oak-core][1] - Oak repository API and implementation + - oak-jcr - JCR binding for the Oak repository + - oak-sling - integration with Apache Sling + - oak-http - HTTP binding for Oak + - oak-run - runnable jar packaging + - oak-it - integration tests + - oak-it/mk - integration tests for MicroKernel + - oak-it/osgi - integration tests for OSGi + - oak-bench - performance tests + + [1]: oak-core/README.md + +License +------- + +(see [LICENSE.txt](LICENSE.txt) for full license details) + +Collective work: Copyright 2012 The Apache Software Foundation. + +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/README.txt b/README.txt deleted file mode 100644 index a09961ef883..00000000000 --- a/README.txt +++ /dev/null @@ -1,48 +0,0 @@ -============================================ -Oak - the next generation content repository -============================================ - -Oak is an effort implement a scalable and performant hierarchical content -repository for use as the foundation of modern world-class web sites and -other demanding content applications. - -The Oak effort is a part of the Apache Jackrabbit project. -Apache Jackrabbit is a project of the Apache Software Foundation. - - -Getting Started ---------------- - -To get started with Oak, build the latest sources with -Maven 3 and Java 6 (or higher) like this: - - mvn clean install - -The build consists of the following main components: - - oak-parent - parent POM - oak-core - main codebase (incl. unit tests) - oak-run - runnable jar packaging - oak-it - integration tests - oak-bench - performance tests - - -License (see also LICENSE.txt) ------------------------------- - -Collective work: Copyright 2012 The Apache Software Foundation. - -Licensed to the Apache Software Foundation (ASF) under one or more -contributor license agreements. See the NOTICE file distributed with -this work for additional information regarding copyright ownership. -The ASF licenses this file to You under the Apache License, Version 2.0 -(the "License"); you may not use this file except in compliance with -the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt new file mode 100644 index 00000000000..7847d0643f5 --- /dev/null +++ b/RELEASE-NOTES.txt @@ -0,0 +1,301 @@ +Release Notes -- Apache Jackrabbit Oak -- Version 0.5 + +Introduction +------------ + +Jackrabbit Oak is an effort to implement a scalable and performant hierarchical content +repository for use as the foundation of modern world-class web sites and +other demanding content applications. + +The Oak effort is a part of the Apache Jackrabbit project. +Apache Jackrabbit is a project of the Apache Software Foundation. + +Jackrabbit Oak 0.5 is to be considered alpha-level software. Use at your own risk +with no stability or compatibility guarantees. + +Changes in Oak 0.5 +------------------ + +Improvements + +[OAK-239] - MicroKernel.getRevisionHistory: maxEntries behavior should be documented +[OAK-255] - Implement Node#getReferences() both for REFERENCE and WEAKREFERENCE +[OAK-258] - Dummy implementation for session scoped locks +[OAK-263] - Type of bindings should be covariant in SessionQueryEngine.executeQuery() +[OAK-264] - MicroKernel.diff for depth limited, unspecified changes +[OAK-274] - Split NodeFilter into its own class +[OAK-275] - Introduce TreeLocation interface +[OAK-282] - Use random port in oak-run tests +[OAK-284] - Reduce memory usage of KernelNodeState +[OAK-285] - Split CommitEditor into CommitEditor and Validator interfaces +[OAK-289] - Remove TreeImpl.Children +[OAK-290] - Move Query related interfaces in oak.spi.query +[OAK-292] - Use Guava preconditions instead of asserts to enforce contract +[OAK-315] - Separate built-in node types from ReadWriteNodeTypeManager + +Bug fixes + +[OAK-136] - NodeDelegate leakage from NodeImpl +[OAK-221] - Clarify nature of 'path' parameter in oak-api +[OAK-228] - inconsistent paths used in oak tests +[OAK-229] - Review root-node shortcut in NamePathMapperImpl +[OAK-230] - Review and fix inconsistent usage of oak-path in oak-jcr +[OAK-238] - ValueFactory: Missing identifier validation when creating (weak)reference value from String +[OAK-240] - mix:mergeConflict violates naming convention +[OAK-242] - Mixin rep:MergeConflict is not a registered node type +[OAK-243] - NodeImpl.getParent() not fully encapsulated in a SessionOperation +[OAK-245] - Add import for org.h2 in oak-mk bundle +[OAK-248] - Review path constants in the oak source code +[OAK-252] - Stop sending observation events on shutdown +[OAK-254] - waitForCommit returns null in certain situations +[OAK-256] - JAAS Authentication failing in OSGi env due to classloading issue +[OAK-257] - NPE in o.a.j.oak.security.privilege.PrivilegeDefinitionImpl constructor +[OAK-265] - waitForCommit gets wrongly triggered on private branch commits +[OAK-268] - XPathQueryEvaluator generates incorrect XPath query +[OAK-272] - every session login causes a mk.branch operation +[OAK-278] - Tree.getStatus() and Tree.getPropertyStatus() fail for items whose parent has been removed +[OAK-279] - ChangeProcessor getting stuck while shutdown +[OAK-286] - Possible NPE in LuceneIndex +[OAK-287] - PrivilegeManagerImplTest.testJcrAll assumes that there are no custom privileges +[OAK-291] - Clarify paths in Root and Tree +[OAK-294] - nt:propertyDefinition has incorrect value constraints for property types +[OAK-296] - PathUtils.isAncestor("/", "/") should return false but returns true +[OAK-299] - Node Type support: SQL2QueryResultTest fails +[OAK-311] - Remapping a namespace breaks existing content +[OAK-313] - Trailing slash not removed for simple path in JCR to Oak path conversion +[OAK-316] - CommitFailedException.throwRepositoryException swallows parts of the stack traces +[OAK-330] - Some MongoMK tests do not use CommitImpl constructor correctly +[OAK-332] - [MongoMK] Node is not visible in head revision +[OAK-334] - Add read-only lucene directory + +Changes in Oak 0.4 +------------------ + +New Features + + [OAK-182] - Support for "invisible" internal content + [OAK-193] - TODO class for partially implemented features + [OAK-227] - MicroKernel API: add depth parameter to diff method + +Improvements + + [OAK-153] - Split the CommitHook interface + [OAK-156] - Observation events need Session.refresh + [OAK-158] - Specify fixed memory settings for unit and integration tests + [OAK-161] - Refactor Tree#getChildStatus + [OAK-163] - Move the JCR TCK back to the integrationTesting profile + [OAK-164] - Replace Tree.remove(String) with Tree.remove() + [OAK-165] - NodeDelegate should not use Tree.getChild() but rather Root.getTree() + [OAK-166] - Add Tree.isRoot() method instead of relying on Tree.getParent() == null + [OAK-171] - Add NodeState.compareAgainstBaseState() + [OAK-172] - Optimize KernelNodeState equality checks + [OAK-174] - Refactor RootImpl and TreeImpl to take advantage of the child node state builder introduced with OAK-170 + [OAK-176] - Reduce CoreValueFactoryImpl footprint + [OAK-183] - Remove duplicate fields from NodeImpl and PropertyImpl which are already in the ItemImpl super class + [OAK-184] - Allow PropertyState.getValues() to work on single-valued properties + [OAK-186] - Avoid unnecessary rebase operations + [OAK-192] - Define behavior of Tree#getParent() if the parent is not accessible + [OAK-194] - Define behavior of Tree#getProperty(String) in case of lack of access + [OAK-195] - State that Tree#hasProperty returns false of the property is not accessible + [OAK-196] - Make Root interface permission aware + [OAK-198] - Refactor RootImpl#merge + [OAK-199] - KernelNodeStore defines 2 access methods for the CommitEditor + [OAK-200] - Replace Commons Collections with Guava + [OAK-232] - Hardcoded "childOrder" in NodeDelegate + +Bug fixes + + [OAK-155] - Query: limited support for the deprecated JCR 1.0 query language Query.SQL + [OAK-173] - MicroKernel filter syntax is not proper JSON + [OAK-177] - Too fast timeout in MicroKernelIT.waitForCommit + [OAK-179] - Tests should not fail if there is a jcr:system node + [OAK-185] - Trying to remove a missing property throws PathNotFoundException + [OAK-187] - ConcurrentModificationException during gc run + [OAK-188] - Invalid JSOP encoding in CommitBuilder and KernelNodeStoreBranch + [OAK-207] - TreeImpl#getStatus() never returns REMOVED + [OAK-208] - RootImplFuzzIT test failures + [OAK-209] - BlobStore: use SHA-256 instead of SHA-1, and use two directory levels for FileBlobStore + [OAK-211] - CompositeEditor should keep the base node state stable + [OAK-213] - Misleading exception message in NodeImpl#getParent + [OAK-215] - Make definition of ItemDelegate#getParent permission aware + [OAK-219] - SessionDelegate#getRoot throws IllegalStateException if the root node is not accessible + [OAK-224] - Allow the ContentRepositoryImpl to receive a CommitEditor in the constructor + +Changes in Oak 0.3 +------------------ + +New Features + + [OAK-9] - Internal tree builder + [OAK-12] - Implement a test suite for the MicroKernel + [OAK-33] - Values in oak-core + [OAK-45] - Add support for branching and merging of private copies to MicroKernel + [OAK-68] - Extension point for commit validation + [OAK-75] - specify format and semantics of 'filter' parameter in MicroKernel API + [OAK-100] - Proper CommitHook handling in NodeStore + [OAK-119] - Oak performance benchmark + [OAK-133] - Session.refresh(true) should allow for manual conflict reconciliation + +Improvements + + [OAK-15] - Clean up oak-jcr + [OAK-19] - Consolidate JSON utilities + [OAK-32] - Drop MicroKernel.dispose() + [OAK-40] - Define session-info like user identification for communication with oak-api + [OAK-54] - IOUtils.readVarInt and readVarLong can result in an endless loop on EOF + [OAK-65] - Naming of NodeState and related classes + [OAK-80] - Implement batched writing for KernelNodeStore + [OAK-84] - Delegates for Session, Node, Property and Item + [OAK-86] - Make setProperty methods of NodeStateBuilder and Tree return the affected property + [OAK-87] - Declarative services and OSGi configuration + [OAK-89] - Improve exception handling + [OAK-92] - Remove org.apache.jackrabbit.mk.HelloWorld + [OAK-96] - PathUtils should use assertions to enable validation instead of system property + [OAK-97] - Implement Item.toString() for logging and debugging purposes + [OAK-102] - Expose the branch feature from NodeStore + [OAK-106] - Use NodeStateBuilder instances to record changes in TreeImpl + [OAK-109] - Efficient diffing against the base node state + [OAK-112] - Refactor ModifiedNodeState and related classes to use type safe iterator utilities + [OAK-113] - drop MicroKernel getNodes(String, String) convenience signature + [OAK-115] - ItemDelegate and sub classes should throw IllegalItemStateException on stale items + [OAK-116] - MicroKernel API: clarify semantics of getNodes depth, offset and count parameters + [OAK-120] - MicroKernel API: specific retention policy of binaries + [OAK-122] - Performance test suite + [OAK-126] - remove unused code + [OAK-138] - Move client/server package in oak-mk to separate project + [OAK-145] - Set up Travis CI builds + [OAK-142] - MicroKernel API: returning the :hash property should be optional + [OAK-143] - Refactor conflict reconciliation from OAK-133: move inner classes to o.a.j.oak.plugins.value + [OAK-148] - Drop feature checks from WorkspaceImpl + [OAK-149] - Automatic session refresh after namespace registry changes + [OAK-151] - Merge oak-it-jcr to oak-jcr + [OAK-159] - Do not use in memory Microkernel for TCK + +Bug fixes + + [OAK-16] - Proper ValueFactory implementation and Value handling + [OAK-43] - Incomplete journal when move and copy operations are involved + [OAK-47] - Wrong results and NPE with copy operation + [OAK-49] - Session.getRepository() should return the object through which the Session was acquired + [OAK-55] - Provide reasonable way to set property on NodeStateEditor + [OAK-58] - connection leak in h2 persistence + [OAK-60] - occasional test case failure DbBlobStoreTest#testGarbageCollection + [OAK-73] - JsopReader and JsopWriter lack javadocs + [OAK-79] - Copy operation misses some child nodes + [OAK-83] - Copy operation would recurse indefinitely if memory permitted + [OAK-85] - NPE and wrong result on copy operation + [OAK-93] - Tree has wrong parent after move + [OAK-94] - oak-it/osgi fails due to required packages not being exported + [OAK-95] - path mapping needs to deal with relative paths + [OAK-99] - reading binary content fails for certain types of content + [OAK-105] - Workspace move operation should not do sanity checks in the scope of the current session + [OAK-110] - NPE in KernelNodeStoreBranch.diffToJsop + [OAK-121] - Occasional test failure in MicroKernelIT.testBlobs: java.net.SocketException: Broken pipe + [OAK-130] - Unexpected result of MicroKernel#getJournal after MicroKernel#merge + [OAK-131] - Session.save() silently discards pending changes + [OAK-134] - Session.save() should do an implicit refresh(true) + [OAK-135] - Better support for RangeIterators + [OAK-139] - Remove JsonBuilder + [OAK-146] - Wrong value passed to before parameter of CommitHook.afterCommit in KernelNodeStore.merge + [OAK-147] - Incorrect Comparator in CommitBuilder.persistStagedNodes + +Changes in Oak 0.2.1 +------------------ + +New features + + [OAK-59] - Implement Session.move + [OAK-63] - Implement workspace copy and move + +Improvements + + [OAK-29] - Simplify SessionContext + [OAK-30] - Strongly typed wrapper for the MicroKernel + [OAK-31] - In-memory MicroKernel for testing + [OAK-44] - Release managements tweaks + [OAK-46] - Efficient diffing of large child node lists + [OAK-48] - MicroKernel.getNodes() should return null for not existing nodes instead of throwing an exception + [OAK-52] - Create smoke-test build profile + [OAK-53] - exclude longer running tests in the default maven profile + [OAK-67] - Initial OSGi Bundle Setup + [OAK-70] - MicroKernelInputStream test and optimization + [OAK-71] - Logging dependencies + [OAK-81] - Remove offset and count parameters from NodeState.getChildNodeEntries() + +Bug fixes + + [OAK-20] - Remove usages of MK API from oak-jcr + [OAK-62] - ConnectionImpl should not acquire Microkernel instance + [OAK-69] - oak-run fails with NPE + [OAK-78] - waitForCommit() test failure for MK remoting + [OAK-82] - Running MicroKernelIT test with the InMem persistence creates a lot of GC threads + +Changes in Oak 0.1 +------------------ + +New features + + [OAK-3] - Internal tree model + [OAK-4] - Runnable jar packaging + [OAK-5] - JCR bindings for Oak + [OAK-6] - Setup integration tests and TCK tests + [OAK-7] - In-memory persistence + +Improvements + + [OAK-1] - Setup basic build structure + [OAK-2] - Use Java 6 as base platform + [OAK-8] - Make return types of NodeState#getProperties() and NodeState#getChildNodeEntries() covariant + [OAK-10] - Impedance mismatch between signatures of NodeState#getChildeNodeEntries and MicroKernel#getNodes + [OAK-24] - Separate component for the microkernel + [OAK-25] - Factor repository descriptors into separate class + [OAK-26] - MVCC causes write skew + [OAK-42] - Prepare for first release + +Bug fixes + + [OAK-27] - Remove Authenticator and CredentialsInfo in oak-jcr + [OAK-38] - KernelNodeState should handle multi valued properties + [OAK-39] - KernelNodeState does not handle boolean values correctly + + +For more detailed information about all the changes in this and other +Oak releases, please see the Oak issue tracker at + + https://issues.apache.org/jira/browse/OAK + +Release Contents +---------------- + +This release consists of a single source archive packaged as a zip file. +The archive can be unpacked with the jar tool from your JDK installation. +See the README.md file for instructions on how to build this release. + +The source archive is accompanied by SHA1 and MD5 checksums and a PGP +signature that you can use to verify the authenticity of your download. +The public key used for the PGP signature can be found at +https://svn.apache.org/repos/asf/jackrabbit/dist/KEYS. + +About Apache Jackrabbit Oak +--------------------------- + +Oak is an effort implement a scalable and performant hierarchical content +repository for use as the foundation of modern world-class web sites and +other demanding content applications. + +The Oak effort is a part of the Apache Jackrabbit project. +Apache Jackrabbit is a project of the Apache Software Foundation. + +For more information, visit http://jackrabbit.apache.org/oak + +About The Apache Software Foundation +------------------------------------ + +Established in 1999, The Apache Software Foundation provides organizational, +legal, and financial support for more than 100 freely-available, +collaboratively-developed Open Source projects. The pragmatic Apache License +enables individual and commercial users to easily deploy Apache software; +the Foundation's intellectual property framework limits the legal exposure +of its 2,500+ contributors. + +For more information, visit http://www.apache.org/ diff --git a/assembly.xml b/assembly.xml new file mode 100644 index 00000000000..3cff072e8a6 --- /dev/null +++ b/assembly.xml @@ -0,0 +1,32 @@ + + + src + + zip + + + + ${project.basedir} + + + **/target/** + **/.*/** + + + + \ No newline at end of file diff --git a/check-release.sh b/check-release.sh new file mode 100755 index 00000000000..30e339e938d --- /dev/null +++ b/check-release.sh @@ -0,0 +1,115 @@ +#!/bin/sh + +## +## Licensed to the Apache Software Foundation (ASF) under one or more +## contributor license agreements. See the NOTICE file distributed with +## this work for additional information regarding copyright ownership. +## The ASF licenses this file to You under the Apache License, Version 2.0 +## (the "License"); you may not use this file except in compliance with +## the License. You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + +USERNAME=${1} +VERSION=${2} +SHA=${3} + +if [ -z "$USERNAME" -o -z "$VERSION" -o -z "$SHA" ] +then + echo "Usage: $0 [temp-directory]" + exit +fi + +STAGING="http://people.apache.org/~$USERNAME/oak/$VERSION/" + +WORKDIR=${4:-target/oak-staging-`date +%s`} +mkdir $WORKDIR -p -v + +echo "[INFO] ------------------------------------------------------------------------" +echo "[INFO] DOWNLOAD STAGED REPOSITORY " +echo "[INFO] ------------------------------------------------------------------------" +echo "[INFO] " + +if [ `wget --help | grep "no-check-certificate" | wc -l` -eq 1 ] +then + CHECK_SSL=--no-check-certificate +fi + +wget $CHECK_SSL --wait 1 -nv -r -np "--reject=html,txt" -P "$WORKDIR" -nH "--cut-dirs=3" --ignore-length "${STAGING}" + +echo "[INFO] ------------------------------------------------------------------------" +echo "[INFO] CHECK SIGNATURES AND DIGESTS " +echo "[INFO] ------------------------------------------------------------------------" +echo "[INFO] " + +## 1. check sha from release email against src.zip.sha file + +downloaded_sha=$(cat `find $WORKDIR -type f | grep jackrabbit-oak-$VERSION-src.zip.sha`) +if [ "$SHA" = "$downloaded_sha" ]; then echo "[INFO] Step 1. Release checksum matches provided checksum."; else echo "[ERROR] Step 1. Release checksum does not match provided checksum!"; fi +echo "[INFO] " + +## 2. check signatures on the artifacts +echo "[INFO] Step 2. Check individual files" + +for f in `find ${WORKDIR} -type f | grep '\.\(zip\|rar\|jar\|war\)$'` +do + echo "[INFO] $f" + gpg --verify $f.asc 2>/dev/null + if [ "$?" = "0" ]; then CHKSUM="GOOD"; else CHKSUM="BAD!!!!!!!!"; fi + if [ ! -f "$f.asc" ]; then CHKSUM="----"; fi + echo "gpg: ${CHKSUM}" + + for hash in md5 sha1 + do + tp=`echo $hash | cut -c 1-3` + if [ ! -f "$f.$tp" ] + then + CHKSUM="----" + else + A="`cat $f.$tp 2>/dev/null`" + B="`openssl $hash $f 2>/dev/null | sed 's/.*= *//' `" + if [ "$A" = "$B" ]; then CHKSUM="GOOD (`cat $f.$tp`)"; else CHKSUM="BAD!! : $A not equal to $B"; fi + fi + echo "$tp : ${CHKSUM}" + done +done + +## 3. check tag contents vs src archive contents +echo "[INFO] " +echo "[INFO] Step 3. Check SVN Tag for version $VERSION with src zip file contents" + +echo "[INFO] doing svn checkout, please wait..." +SVNTAGDIR="$WORKDIR/tag-svn/jackrabbit-oak-$VERSION" +svn --quiet export http://svn.apache.org/repos/asf/jackrabbit/oak/tags/jackrabbit-oak-$VERSION $SVNTAGDIR + +echo "[INFO] unzipping src zip file, please wait..." +ZIPTAG="$WORKDIR/tag-zip" +unzip -q $WORKDIR/jackrabbit-oak-$VERSION-src.zip -d $ZIPTAG +ZIPTAGDIR="$ZIPTAG/jackrabbit-oak-$VERSION" + +DIFFOUT=`diff -r $SVNTAGDIR $ZIPTAGDIR` +if [ -n "$DIFFOUT" ] +then + echo "[ERROR] Found some differences!" + echo "$DIFFOUT" +else + echo "[INFO] No differences found." +fi + +## 4. run the build with the pedantic profile to have the rat licence check enabled + +echo "[INFO] ------------------------------------------------------------------------" +echo "[INFO] RUNNING MAVEN BUILD " +echo "[INFO] ------------------------------------------------------------------------" +echo "[INFO] " + +cd "$ZIPTAGDIR" +mvn package -Ppedantic + diff --git a/doc/construct.md b/doc/construct.md new file mode 100644 index 00000000000..a21e9cd3e5f --- /dev/null +++ b/doc/construct.md @@ -0,0 +1,78 @@ + + +# Repository construction + +Oak comes with a simple mechanism for constructing content repositories +for use in embedded deployments and test cases. This article describes this +mechanism. Deployments in managed enviroments like OSGi should use the native +construction/configuration mechanism of the environment. + +The core class to use is called `Oak` and can be found in the +`org.apache.jackrabbit.oak` package inside `oak-core`. It takes a +`MicroKernel` instance and wraps it into a `ContentRepository`: + + MicroKernel kernel = ...; + ContentRepository repository = new Oak(kernel).createContentRepository(); + +For test purposes you can use the default constructor that +automatically instantiates an in-memory `MicroKernel` for use with the +repository. And if you're only using the test repository for a single +`ContentSession` or just a singe `Root`, then you can shortcut the login +steps by using either of the last two statements below: + + ContentRepository repository = new Oak().createContentRepository(); + ContentSession session = new Oak().createContentSession(); + Root root = new Oak().createRoot(); + +By default no pluggable components are associated with the created +repository, so all login attempts will work and result in full write +access. There's also no need to close the sessions or otherwise +release acquired resources, as normal garbage collection will take +care of everything. + +To add extra functionality like type validation or indexing support, +use the `with()` method. The method takes all kinds of Oak plugins and +adds them to the repository to be created. The method returns the Oak +instance being used, so you can chain method calls like this: + + ContentRepository repository = new Oak(kernel) + .with(new InitialContent()) // add initial content + .with(new DefaultTypeEditor()) // automatically set default types + .with(new NameValidatorProvider()) // allow only valid JCR names + .with(new SecurityProviderImpl()) // use the default security + .with(new PropertyIndexHook()) // simple indexing support + .with(new PropertyIndexProvider()) // search support for the indexes + .createContentRepository(); + +As you can see, constructing a fully featured JCR repository like this +will require quite a few plugins. To avoid having to specify them all +whenever constructing a new repository, we also have a class called +`Jcr` in the `org.apache.jakcrabbit.oak.jcr` package in `oak-jcr`. That +class works much like the `Oak` class, but it constructs +`javax.jcr.Repository` instances instead of `ContentRepositories` and +automatically includes all the plugin components needed for proper JCR +functionality: + + MicroKernel kernel = ...; + Repository repository = new Jcr(kernel).createRepository(); + +The `Jcr` class supports all the same `with()` methods as the `Oak` class +does, so you can easily extend the constructed JCR repository with custom +functionality if you like. For test purposes the `Jcr` class also has an +empty default constructor that works like the one in the `Oak` class. + diff --git a/doc/nodestate.md b/doc/nodestate.md new file mode 100644 index 00000000000..b730795f86c --- /dev/null +++ b/doc/nodestate.md @@ -0,0 +1,325 @@ + + +# Understanding the node state model + +This article describes the node state model that is the core design +abstraction inside the oak-core component. Understanding the node state +model is essential to working with Oak internals and to building custom +Oak extensions. + +## Background + +Oak organizes all content in a large tree hierarchy that consists of nodes +and properties. Each snapshot or revision of this content tree is immutable, +and changes to the tree are expressed as a sequence of new revisions. The +MicroKernel of an Oak repository is responsible for managing the content +tree and its revisions. + +The JSON-based MicroKernel API works well as a part of a remote protocol +but is cumbersome to use directly in oak-core. There are also many cases +where transient or virtual content that doesn't (yet) exist in the +MicroKernel needs to be managed by Oak. The node state model as expressed +in the NodeState interface in oak-core is designed for these purposes. It +provides a unified low-level abstraction for managing all tree content and +lays the foundation for the higher-level Oak API that's visible to clients. + +## The state of a node + +A _node_ in Oak is an unordered collection of named properties and child +nodes. As the content tree evolves through a sequence of revisions, a node +in it will go through a series of different states. A _node state_ then is +an _immutable_ snapshot of a specific state of a node and the subtree beneath +it. + +To avoid making a special case of the root node and therefore to make it +easy to write algorithms that can recursively process each subtree as a +standalone content tree, a node state is _unnamed_ and does not contain +information about it's location within a larger content tree. Instead each +property and child node state is uniquely named within a parent node state. +An algorithm that needs to know the path of a node can construct it from +the encountered names as it descends the tree structure. + +Since node states are immutable, they are also easy to keep _thread-safe_. +Implementations that use mutable data structures like caches or otherwise +aren't thread-safe by default, are expected to use other mechanisms like +synchronization to ensure thread-safety. + +## The NodeState interface + +The above design principles are reflected in the `NodeState` interface +in the `org.apache.jackrabbit.oak.spi.state` package of oak-core. The +interface consists of three sets of methods: + + * Methods for accessing properties + * Methods for accessing child nodes + * The `builder` method for building modified states + * The `compareAgainstBaseState` method for comparing states + +You can request a property or a child node by name, get the number of +properties or child nodes, or iterate through all of them. Even though +properties and child nodes are accessed through separate methods, they +share the same namespace so a given name can either refer to a property +or a child node, but not to both at the same time. + +Iteration order of properties and child nodes is _unspecified but stable_, +so that re-iterating through the items of a _specific NodeState instance_ +will return the items in the same order as before, but the specific ordering +is not defined nor does it necessarily remain the same across different +instances. + +The last two methods, `builder` and `compareAgainstBaseState`, are +covered in the next two sections. See also the `NodeState` javadocs for +more details about this interface and all its methods. + +## Building new node states + +Since node states are immutable, a separate builder interface, +`NodeBuilder`, is used to construct new, modified node states. Calling +the `builder` method on a node state returns such a builder for +modifying that node and the subtree below it. + +A node builder can be thought of as a _mutable_ version of a node state. +In addition to property and child node access methods like the ones that +are already present in the `NodeState` interface, the `NodeBuilder` +interface contains the following key methods: + + * The `setProperty` and `removeProperty` methods for modifying properties + * The `removeNode` method for removing a subtree + * The `setNode` method for adding or replacing a subtree + * The `child` method for creating or modifying a subtree with + a connected child builder + * The `getNodeState` method for getting a frozen snapshot of the modified + content tree + +The concept of _connected builders_ is designed to make it easy to manage +complex content changes. Since individual node states are always immutable, +modifying a particular node at a path like `/foo/bar` using the `setNode` +method would require the following overly verbose code: + + NodeState root = …; + NodeState foo = root.getChildNode("foo") + NodeState bar = foo.getChildNode("bar"); + NodeBuilder barBuilder = bar.builder(); + barBuilder.setProperty("test", …); + NodeBuilder fooBuilder = foo.builder(); + fooBuilder.setNode("bar", barBuilder.getNodeState()); + NodeBuilder rootBuilder = root.builder(); + rootBuilder.setNode("foo", fooBuilder.getNodeState()); + root = rootBuilder.getNodeState(); + +The complexity here is caused by the need to explicitly construct and +re-connect each modified node state along the path from the root to the +modified content in `/foo/bar`. This is because each `NodeBuilder` instance +created by the `getBuilder` method is independent and can only be used to +affect other builders in the manner shown above. In contrast the +`child` method returns a builder instance that is "connected" to +the parent builder in a way that any changes recorded in the child builder +will automatically show up also in the node states created by the parent +builder. With connected builders the above code can be simplified to: + + NodeState root = …; + NodeBuilder rootBuilder = root.builder(); + rootBuilder + .child("foo") + .child("bar") + .setProperty("test", …); + root = rootBuilder.getNodeState(); + +Typically the only case where the `setNode` method is preferable over +`child` is when moving or copying subtrees from one location +to another. For example, the following code copies the `/orig` subtree +to `/copy`: + + NodeState root = …; + NodeBuilder rootBuilder = root.builder(); + rootBuilder.setNode("copy", root.getChildNode("orig")); + root = rootBuilder.getNodeState(); + +The node states constructed by a builder often retain an internal reference +to the base state used by the builder. This allows common node state +comparisons to perform really well as described in the next section. + +## Comparing node states + +As a node evolves through a sequence of states, it's often important to +be able to tell what has changed between two states of the node. This +functionality is available through the `compareAgainstBaseState` method. +The method takes two arguments: + + * A _base state_ for the comparison. The comparison will report all + changes necessary for moving from the given base state to the node + state on which the comparison method is invoked. + * A `NodeStateDiff` instance to which all detected changes are reported. + The diff interface contains callback methods for reporting added, + modified or removed properties or child nodes. + +The comparison method can actually be used to compare any two nodes, but the +implementations of the method are typically heavily optimized for the case +when the given base state actually is an earlier version of the same node. +In practice this is by far the most common scenario for node state comparisons, +and can typically be executed in `O(d)` time where `d` is the number of +changes between the two states. The fallback strategy for comparing two +completely unrelated node states can be much more expensive. + +An important detail of the `NodeStateDiff` mechanism is the `childNodeChanged` +method that will get called if there are _any_ changes in the subtree starting +at the named child node. The comparison method should thus be able to +efficiently detect differences at any depth below the given nodes. On the +other hand the `childNodeChanged` method is called only for the direct child +node, and the diff implementation should explicitly recurse down the tree +if it wants to know what exactly did change under that subtree. The code +for such recursion typically looks something like this: + + public void childNodeChanged( + String name, NodeState before, NodeState after) { + after.compareAgainstBaseState(before, ...); + } + +## The commit hook mechanism + +TODO + +## Commit validation + +TODO + +TODO: Basic validator class + + class DenyContentWithName extends DefaultValidator { + + private final String name; + + public DenyContentWithName(String name) { + this.name = name; + } + + @Override + public void propertyAdded(PropertyState after) + throws CommitFailedException { + if (name.equals(after.getName())) { + throw new CommitFailedException( + "Properties named " + name + " are not allowed"); + } + } + + + } + +TODO: Example of how the validator works + + Repository repository = new Jcr() + .with(new DenyContentWithName("bar")) + .createRepository(); + + Session session = repository.login(); + Node root = session.getRootNode(); + root.setProperty("foo", "abc"); + session.save(); + root.setProperty("bar", "def"); + session.save(); // will throw an exception + +TODO: Extended example that also works below root and covers also node names + + class DenyContentWithName extends DefaultValidator { + + private final String name; + + public DenyContentWithName(String name) { + this.name = name; + } + + private void testName(String addedName) throws CommitFailedException { + if (name.equals(addedName)) { + throw new CommitFailedException( + "Content named " + name + " is not allowed"); + } + } + + @Override + public void propertyAdded(PropertyState after) + throws CommitFailedException { + testName(after.getName()); + } + + @Override + public Validator childNodeAdded(String name, NodeState after) + throws CommitFailedException { + testName(name); + return this; + } + + @Override + public Validator childNodeChanged( + String name, NodeState before, NodeState after) + throws CommitFailedException { + return this; + } + + } + +## Commit modification + +TODO + +TODO: Basic commit hook example + + class RenameContentHook implements CommitHook { + + private final String name; + + private final String rename; + + public RenameContentHook(String name, String rename) { + this.name = name; + this.rename = rename; + } + + @Override @Nonnull + public NodeState processCommit(NodeState before, NodeState after) + throws CommitFailedException { + PropertyState property = after.getProperty(name); + if (property != null) { + NodeBuilder builder = after.builder(); + builder.removeProperty(name); + if (property.isArray()) { + builder.setProperty(rename, property.getValues()); + } else { + builder.setProperty(rename, property.getValue()); + } + return builder.getNodeState(); + } + return after; + } + + } + +TODO: Using the commit hook to avoid the exception from a validator + + Repository repository = new Jcr() + .with(new RenameContentHook("bar", "foo")) + .with(new DenyContentWithName("bar")) + .createRepository(); + + Session session = repository.login(); + Node root = session.getRootNode(); + root.setProperty("foo", "abc"); + session.save(); + root.setProperty("bar", "def"); + session.save(); // will not throw an exception! + System.out.println(root.getProperty("foo").getString()); // Prints "def"! + diff --git a/oak-bench/README.txt b/oak-bench/README.txt new file mode 100644 index 00000000000..bc4dbdd66c7 --- /dev/null +++ b/oak-bench/README.txt @@ -0,0 +1,85 @@ +--------------------------------- +Oak Performance Test Suite +--------------------------------- + +This directory contains a simple performance test suite that can be +extended for ongoing Oak versions and micro kernels. Use the following +command to run this test suite: + + mvn clean install + +Note that the test suite will take more than an hour to complete, and to +avoid distorting the results you should avoid putting any extra load on +the computer while the test suite is running. + +The results are stored as oak*/target/*.txt report files and can +be combined into an HTML report by running the following command on a +(Unix) system where gnuplot is installed. + + sh plot.sh + +Mac OS X note : if you want to execute the above script, you will need +to install gnuplot and imagemagick2-svg from the Fink project. For +more information : http://finkproject.org + +Selecting which tests to run +---------------------------- + +The -Donly command line parameter allows you to specify a regexp for +selecting which performance test cases to run. To run a single test +case, use a command like this: + + mvn clean install -Donly=ConcurrentReadTest + +To run all concurrency tests, use: + + mvn clean install -Donly=Concurrent.*Test + +Selecting which micro kernel to test +---------------------------------------------------------- + +The -Dmk command line parameter allows you to specify a regexp for +selecting the micro kernel and configurations against which the +performance tests are run. The default setting selects only the default +micro kernel: + + mvn clean install -Dmk=\d\.\d + +To run the tests against all included configurations, use: + + mvn clean install -Dmk=.* + +Using a profiler +---------------- + +To enable a profiler, use the -Dagentlib= command line pameter: + + mvn clean install -Dagentlib=hprof=cpu=samples,depth=10 + +Adding a new performance test +----------------------------- + +The tests run by this performance test suite are listed in the +testPerformance() method of the AbstractPerformanceTest class in +the org.apache.jackrabbit.oak.performance package of the oak-perf-base +component that you can find in the ./base directory. + +Each test is a subclass of the AbstractTest class in that same package, +and you need to implement at least the abstract runTest() method when +creating a new test. The runTest() method should contain the code whose +performance you want to measure. For best measurement results the method +should normally take something between 0.1 to 10 seconds to execute, so +you may need to add a constant-size loop around your code like is done +for example in the LoginTest class. The test suite compares relative +performance between different Oak versions, so the absolute time +taken by the test method is irrelevant. + +Many performance tests need some setup and teardown code for things like +building the content tree against which the test is being run. Such work +should not be included in the runTest() method to prevent affecting the +performance measurements. Instead you can override the before/afterTest() +and before/afterSuite() methods that get called respectively before and +after each individual test iteration and the entire test suite. See for +example the SetPropertyTest class for an example of how these methods +are best used. + diff --git a/oak-bench/base/pom.xml b/oak-bench/base/pom.xml new file mode 100644 index 00000000000..a1a15e2b247 --- /dev/null +++ b/oak-bench/base/pom.xml @@ -0,0 +1,77 @@ + + + + + + 4.0.0 + + + org.apache.jackrabbit + oak-bench-parent + 0.6-SNAPSHOT + ../parent/pom.xml + + + oak-bench-base + Oak Performance Test Utilities + + + + javax.jcr + jcr + 2.0 + + + org.apache.commons + commons-math + 2.0 + + + org.apache.jackrabbit + oak-jcr + ${project.version} + provided + + + com.h2database + h2 + 1.3.158 + + + commons-io + commons-io + 1.4 + + + org.slf4j + slf4j-api + 1.5.8 + + + org.slf4j + slf4j-nop + 1.5.8 + + + junit + junit + + + + + diff --git a/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/AbstractPerformanceTest.java b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/AbstractPerformanceTest.java new file mode 100644 index 00000000000..54891999fcb --- /dev/null +++ b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/AbstractPerformanceTest.java @@ -0,0 +1,167 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.performance; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.regex.Pattern; +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.SimpleCredentials; + +import org.apache.commons.io.output.FileWriterWithEncoding; +import org.apache.commons.math.stat.descriptive.DescriptiveStatistics; +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +import org.apache.jackrabbit.oak.jcr.Jcr; + +/** + * This class calls all known performance tests. + */ +public abstract class AbstractPerformanceTest { + + /** + * The warmup time, in ms. + */ + private final int warmup = Integer.getInteger("oak.performanceTest.warmup", 0); + + /** + * How long each test is repeated, in ms. + */ + private final int runtime = Integer.getInteger("oak.performanceTest.runtime", 100); + + private final Credentials credentials = new SimpleCredentials("admin", + "admin".toCharArray()); + + private final Pattern microKernelPattern = Pattern.compile(System + .getProperty("mk", ".*")); + private final Pattern testPattern = Pattern.compile(System.getProperty( + "only", ".*")); + + protected void testPerformance(String name, String microKernel) + throws Exception { + + runTest(new LoginTest(), name, microKernel); + runTest(new LoginLogoutTest(), name, microKernel); + runTest(new ReadPropertyTest(), name, microKernel); + runTest(new SetPropertyTest(), name, microKernel); + runTest(new SmallFileReadTest(), name, microKernel); + runTest(new SmallFileWriteTest(), name, microKernel); + runTest(new ConcurrentReadTest(), name, microKernel); + runTest(new ConcurrentReadWriteTest(), name, microKernel); + runTest(new SimpleSearchTest(), name, microKernel); + runTest(new SQL2SearchTest(), name, microKernel); + runTest(new DescendantSearchTest(), name, microKernel); + runTest(new SQL2DescendantSearchTest(), name, microKernel); + runTest(new CreateManyChildNodesTest(), name, microKernel); + runTest(new UpdateManyChildNodesTest(), name, microKernel); + runTest(new TransientManyChildNodesTest(), name, microKernel); + + } + + private void runTest(AbstractTest test, String name, String microKernel) { + if (microKernelPattern.matcher(microKernel).matches() + && testPattern.matcher(test.toString()).matches()) { + + MicroKernel mk = createMicroKernel(microKernel); + try { + Repository repository= createRepository(mk); + + // Run the test + DescriptiveStatistics statistics = runTest(test, repository); + if (statistics.getN() > 0) { + writeReport(test.toString(), name, microKernel, statistics); + } + } catch (RepositoryException re) { + re.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + disposeMicroKernel(mk); + } + } + } + + private DescriptiveStatistics runTest(AbstractTest test, + Repository repository) throws Exception { + DescriptiveStatistics statistics = new DescriptiveStatistics(); + + test.setUp(repository, credentials); + try { + // Run a few iterations to warm up the system + if (warmup > 0) { + long warmupEnd = System.currentTimeMillis() + warmup; + while (System.currentTimeMillis() < warmupEnd) { + test.execute(); + } + } + + // Run test iterations, and capture the execution times + long runtimeEnd = System.currentTimeMillis() + runtime; + while (System.currentTimeMillis() < runtimeEnd) { + statistics.addValue(test.execute()); + } + } finally { + test.tearDown(); + } + + return statistics; + } + + private static void writeReport(String test, String name, String microKernel, + DescriptiveStatistics statistics) throws IOException { + File report = new File("target", test + "-" + microKernel + ".txt"); + + boolean needsPrefix = !report.exists(); + PrintWriter writer = new PrintWriter(new FileWriterWithEncoding(report, + "UTF-8", true)); + try { + if (needsPrefix) { + writer.format( + "# %-34.34s min 10%% 50%% 90%% max%n", + test); + } + + writer.format("%-36.36s %6.0f %6.0f %6.0f %6.0f %6.0f%n", + name, statistics.getMin(), statistics.getPercentile(10.0), + statistics.getPercentile(50.0), + statistics.getPercentile(90.0), statistics.getMax()); + } finally { + writer.close(); + } + } + + protected MicroKernel createMicroKernel(String microKernel) { + + // TODO: depending on the microKernel string a particular repository + // with that MK must be returned + + return new MicroKernelImpl("target/mk-tck-" + System.currentTimeMillis()); + + } + + protected void disposeMicroKernel(MicroKernel kernel) { + ((MicroKernelImpl) kernel).dispose(); + } + + protected Repository createRepository(MicroKernel mk) { + return new Jcr(mk).createRepository(); + } + +} diff --git a/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/AbstractTest.java b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/AbstractTest.java new file mode 100644 index 00000000000..cf70617b66a --- /dev/null +++ b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/AbstractTest.java @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.performance; + +import java.util.LinkedList; +import java.util.List; + +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** + * Abstract base class for individual performance benchmarks. + */ +public abstract class AbstractTest { + + private Repository repository; + + private Credentials credentials; + + private List sessions; + + private List threads; + + private volatile boolean running; + + protected static int getScale(int def) { + int scale = Integer.getInteger("scale", 0); + if (scale == 0) { + scale = def; + } + return scale; + } + + /** + * Prepares this performance benchmark. + * + * @param repository the repository to use + * @param credentials credentials of a user with write access + * @throws Exception if the benchmark can not be prepared + */ + public void setUp(Repository repository, Credentials credentials) + throws Exception { + this.repository = repository; + this.credentials = credentials; + this.sessions = new LinkedList(); + this.threads = new LinkedList(); + + this.running = true; + + beforeSuite(); + } + + /** + * Executes a single iteration of this test. + * + * @return number of milliseconds spent in this iteration + * @throws Exception if an error occurs + */ + public long execute() throws Exception { + beforeTest(); + try { + long start = System.currentTimeMillis(); + // System.out.println("execute " + this); + runTest(); + return System.currentTimeMillis() - start; + } finally { + afterTest(); + } + } + /** + * Cleans up after this performance benchmark. + * + * @throws Exception if the benchmark can not be cleaned up + */ + public void tearDown() throws Exception { + this.running = false; + for (Thread thread : threads) { + thread.join(); + } + + afterSuite(); + + for (Session session : sessions) { + if (session.isLive()) { + session.logout(); + } + } + + this.threads = null; + this.sessions = null; + this.credentials = null; + this.repository = null; + } + + /** + * Run before any iterations of this test get executed. Subclasses can + * override this method to set up static test content. + * + * @throws Exception if an error occurs + */ + protected void beforeSuite() throws Exception { + } + + protected void beforeTest() throws Exception { + } + + protected abstract void runTest() throws Exception; + + protected void afterTest() throws Exception { + } + + /** + * Run after all iterations of this test have been executed. Subclasses can + * override this method to clean up static test content. + * + * @throws Exception if an error occurs + */ + protected void afterSuite() throws Exception { + } + + protected void failOnRepositoryVersions(String... versions) + throws RepositoryException { + String repositoryVersion = + repository.getDescriptor(Repository.REP_VERSION_DESC); + for (String version : versions) { + if (repositoryVersion.startsWith(version)) { + throw new RepositoryException( + "Unable to run " + getClass().getName() + + " on repository version " + version); + } + } + } + + protected Repository getRepository() { + return repository; + } + + protected Credentials getCredentials() { + return credentials; + } + + /** + * Returns a new reader session that will be automatically closed once + * all the iterations of this test have been executed. + * + * @return reader session + */ + protected Session loginReader() { + try { + Session session = repository.login(); + sessions.add(session); + return session; + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns a new writer session that will be automatically closed once + * all the iterations of this test have been executed. + * + * @return writer session + */ + protected Session loginWriter() { + try { + Session session = repository.login(credentials); + sessions.add(session); + return session; + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + + /** + * Adds a background thread that repeatedly executes the given job + * until all the iterations of this test have been executed. + * + * @param job background job + */ + protected void addBackgroundJob(final Runnable job) { + Thread thread = new Thread("Background job " + job) { + @Override + public void run() { + while (running) { + try { + // rate-limit, to avoid 100% cpu usage + Thread.sleep(10); + } catch (InterruptedException e) { + // ignore + } + job.run(); + } + } + }; + thread.setDaemon(true); + thread.setPriority(Thread.MIN_PRIORITY); + thread.start(); + threads.add(thread); + } + + public String toString() { + String name = getClass().getName(); + return name.substring(name.lastIndexOf('.') + 1); + } + +} diff --git a/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/ConcurrentReadTest.java b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/ConcurrentReadTest.java new file mode 100644 index 00000000000..1964fd0bac2 --- /dev/null +++ b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/ConcurrentReadTest.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.performance; + +import java.util.Random; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +/** + * Test case that traverses 10k unstructured nodes (100x100) while 50 concurrent + * readers randomly access nodes from within this tree. + */ +public class ConcurrentReadTest extends AbstractTest { + + protected static final int NODE_COUNT = 100; + + private static final int READER_COUNT = getScale(20); + + private Session session; + + protected Node root; + + @Override + public void beforeSuite() throws Exception { + session = getRepository().login( + new SimpleCredentials("admin", "admin".toCharArray())); + root = session.getRootNode().addNode("testroot", "nt:unstructured"); + for (int i = 0; i < NODE_COUNT; i++) { + Node node = root.addNode("node" + i, "nt:unstructured"); + for (int j = 0; j < NODE_COUNT; j++) { + node.addNode("node" + j, "nt:unstructured"); + } + session.save(); + } + + for (int i = 0; i < READER_COUNT; i++) { + addBackgroundJob(new Reader()); + } + } + + class Reader implements Runnable { + + private Session session; + + private final Random random = new Random(); + + public void run() { + + try { + session = getRepository().login( + new SimpleCredentials("admin", "admin".toCharArray())); + int i = random.nextInt(NODE_COUNT); + int j = random.nextInt(NODE_COUNT); + session.getRootNode() + .getNode("testroot/node" + i + "/node" + j); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + + } + + @Override + public void runTest() throws Exception { + Reader reader = new Reader(); + for (int i = 0; i < 1000; i++) { + reader.run(); + } + } + + @Override + public void afterSuite() throws Exception { + for (int i = 0; i < NODE_COUNT; i++) { + root.getNode("node" + i).remove(); + session.save(); + } + + root.remove(); + session.save(); + } + +} diff --git a/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/ConcurrentReadWriteTest.java b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/ConcurrentReadWriteTest.java new file mode 100644 index 00000000000..03b89af3510 --- /dev/null +++ b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/ConcurrentReadWriteTest.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.performance; + +import java.util.Random; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +/** + * A {@link ConcurrentReadTest} with a single writer thread that continuously + * updates the nodes being accessed by the readers. + */ +public class ConcurrentReadWriteTest extends ConcurrentReadTest { + + @Override + public void beforeSuite() throws Exception { + super.beforeSuite(); + + addBackgroundJob(new Writer()); + } + + class Writer implements Runnable { + + private Session session; + + private final Random random = new Random(); + + private long count; + + public void run() { + try { + session = getRepository().login( + new SimpleCredentials("admin", "admin".toCharArray())); + int i = random.nextInt(NODE_COUNT); + int j = random.nextInt(NODE_COUNT); + Node node = session.getRootNode().getNode( + "testroot/node" + i + "/node" + j); + node.setProperty("count", count++); + session.save(); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + + } + +} diff --git a/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/CreateManyChildNodesTest.java b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/CreateManyChildNodesTest.java new file mode 100644 index 00000000000..5c0b367ce87 --- /dev/null +++ b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/CreateManyChildNodesTest.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.performance; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** + * Test for measuring the performance of creating a node with + * {@value #CHILD_COUNT} child nodes. + */ +public class CreateManyChildNodesTest extends AbstractTest { + + private static final int CHILD_COUNT = 10 * 1000; + + private Session session; + + @Override + public void beforeSuite() throws RepositoryException { + session = loginWriter(); + } + + @Override + public void beforeTest() throws RepositoryException { + } + + @Override + public void runTest() throws Exception { + Node node = session.getRootNode().addNode("testnode", "nt:unstructured"); + for (int i = 0; i < CHILD_COUNT; i++) { + node.addNode("node" + i, "nt:unstructured"); + } + session.save(); + } + + @Override + public void afterTest() throws RepositoryException { + session.getRootNode().getNode("testnode").remove(); + session.save(); + } + +} diff --git a/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/DescendantSearchTest.java b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/DescendantSearchTest.java new file mode 100644 index 00000000000..cd7cc1bc648 --- /dev/null +++ b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/DescendantSearchTest.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.performance; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; + +/** + * Performance test to check performance of queries on sub-trees. + */ +public class DescendantSearchTest extends AbstractTest { + + private static final int NODE_COUNT = 100; + + private Session session; + + private Node root; + + protected Query createQuery(QueryManager manager, int i) + throws RepositoryException { + @SuppressWarnings("deprecation") + String xpath = Query.XPATH; + return manager.createQuery("/jcr:root/testroot//element(*,nt:base)[@testcount=" + i + "]", xpath); + } + + @Override + public void beforeSuite() throws RepositoryException { + session = getRepository().login(getCredentials()); + + root = session.getRootNode().addNode("testroot", "nt:unstructured"); + for (int i = 0; i < NODE_COUNT; i++) { + Node node = root.addNode("node" + i, "nt:unstructured"); + for (int j = 0; j < NODE_COUNT; j++) { + Node child = node.addNode("node" + j, "nt:unstructured"); + child.setProperty("testcount", j); + } + session.save(); + } + + IndexManager.createPropertyIndex(session, "testcount"); + + } + + @Override + public void runTest() throws Exception { + QueryManager manager = session.getWorkspace().getQueryManager(); + for (int i = 0; i < NODE_COUNT; i++) { + Query query = createQuery(manager, i); + NodeIterator iterator = query.execute().getNodes(); + while (iterator.hasNext()) { + Node node = iterator.nextNode(); + if (node.getProperty("testcount").getLong() != i) { + throw new Exception("Invalid test result: " + node.getPath()); + } + } + } + } + + @Override + public void afterSuite() throws RepositoryException { + for (int i = 0; i < NODE_COUNT; i++) { + root.getNode("node" + i).remove(); + session.save(); + } + + root.remove(); + session.save(); + session.logout(); + } + +} diff --git a/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/IndexManager.java b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/IndexManager.java new file mode 100644 index 00000000000..6843712c503 --- /dev/null +++ b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/IndexManager.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.performance; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.plugins.index.IndexConstants; + +/** + * A utility class to manage indexes in Oak. + */ +public class IndexManager { + + /** + * The root node of the index definition (configuration) nodes. + */ + public static final String INDEX_CONFIG_PATH = '/' + IndexConstants.INDEX_DEFINITIONS_NAME + "/indexes"; + + /** + * Creates a property index for the given property if such an index doesn't + * exist yet, and if the repository supports property indexes. The session + * may not have pending changes. + * + * @param session the session + * @param propertyName the property name + * @return true if the index was created or already existed + */ + public static boolean createPropertyIndex(Session session, + String propertyName) throws RepositoryException { + return createIndex(session, "property@" + propertyName); + } + + private static Node getIndexNode(Session session) + throws RepositoryException { + Node n = session.getRootNode(); + for (String e : PathUtils.elements(INDEX_CONFIG_PATH)) { + if (!n.hasNode(e)) { + return null; + } + n = n.getNode(e); + } + return n; + } + + private static boolean createIndex(Session session, String indexNodeName) + throws RepositoryException { + if (session.hasPendingChanges()) { + throw new RepositoryException("The session has pending changes"); + } + Node indexes = getIndexNode(session); + if (indexes == null) { + return false; + } + if (!indexes.hasNode(indexNodeName)) { + indexes.addNode(indexNodeName); + session.save(); + } + return true; + } + +} diff --git a/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/LoginLogoutTest.java b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/LoginLogoutTest.java new file mode 100644 index 00000000000..a5aa2ba6a6d --- /dev/null +++ b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/LoginLogoutTest.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.performance; + +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +public class LoginLogoutTest extends AbstractTest { + + @Override + public void setUp(Repository repository, Credentials credentials) + throws Exception { + super.setUp(repository, + new SimpleCredentials("admin", "admin".toCharArray())); + } + + @Override + public void runTest() throws RepositoryException { + Repository repository = getRepository(); + for (int i = 0; i < 1000; i++) { + Session session = repository.login(getCredentials()); + try { + session.getRootNode(); + } finally { + session.logout(); + } + } + } + +} diff --git a/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/LoginTest.java b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/LoginTest.java new file mode 100644 index 00000000000..5cfc1108519 --- /dev/null +++ b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/LoginTest.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.performance; + +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +public class LoginTest extends AbstractTest { + + private final Session[] sessions = new Session[1000]; + + @Override + public void setUp(Repository repository, Credentials credentials) + throws Exception { + super.setUp(repository, + new SimpleCredentials("admin", "admin".toCharArray())); + } + + @Override + public void runTest() throws RepositoryException { + for (int i = 0; i < sessions.length; i++) { + sessions[i] = getRepository().login(getCredentials(), "default"); + } + + } + + @Override + public void afterTest() throws RepositoryException { + for (Session session : sessions) { + session.logout(); + } + } + +} diff --git a/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/ReadPropertyTest.java b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/ReadPropertyTest.java new file mode 100644 index 00000000000..f3221c3beb0 --- /dev/null +++ b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/ReadPropertyTest.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.performance; + +import javax.jcr.Node; +import javax.jcr.Session; + +/** + * {@code ReadPropertyTest} implements a performance test, which reads + * three properties: one with a jcr prefix, one with the empty prefix and a + * third one, which does not exist. + */ +public class ReadPropertyTest extends AbstractTest { + + private Session session; + + private Node root; + + @Override + protected void beforeSuite() throws Exception { + session = getRepository().login(getCredentials()); + root = session.getRootNode().addNode( + getClass().getSimpleName(), "nt:unstructured"); + root.setProperty("property", "value"); + session.save(); + } + + @Override + protected void runTest() throws Exception { + for (int i = 0; i < 10000; i++) { + root.getProperty("jcr:primaryType"); + root.getProperty("property"); + root.hasProperty("does-not-exist"); + } + } + + @Override + protected void afterSuite() throws Exception { + root.remove(); + session.save(); + session.logout(); + } +} diff --git a/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/SQL2DescendantSearchTest.java b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/SQL2DescendantSearchTest.java new file mode 100644 index 00000000000..6919a81198c --- /dev/null +++ b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/SQL2DescendantSearchTest.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.performance; + +import javax.jcr.RepositoryException; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; + +/** + * SQL-2 version of the sub-tree performance test. + */ +public class SQL2DescendantSearchTest extends DescendantSearchTest { + + @Override + protected Query createQuery(QueryManager manager, int i) + throws RepositoryException { + return manager.createQuery( + "SELECT * FROM [nt:base] AS n WHERE ISDESCENDANTNODE(n, '/testroot') AND testcount=" + i, + "JCR-SQL2"); + } + +} diff --git a/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/SQL2SearchTest.java b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/SQL2SearchTest.java new file mode 100644 index 00000000000..1fc4749a079 --- /dev/null +++ b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/SQL2SearchTest.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.performance; + +import javax.jcr.RepositoryException; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; + +public class SQL2SearchTest extends SimpleSearchTest { + + @Override + protected Query createQuery(QueryManager manager, int i) + throws RepositoryException { + return manager.createQuery( + "SELECT * FROM [nt:base] WHERE testcount=" + i, + "JCR-SQL2"); + } + +} diff --git a/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/SetPropertyTest.java b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/SetPropertyTest.java new file mode 100644 index 00000000000..dff5fef800b --- /dev/null +++ b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/SetPropertyTest.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.performance; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** + * Test for measuring the performance of setting a single property and + * saving the change. + */ +public class SetPropertyTest extends AbstractTest { + + private Session session; + + private Node node; + + @Override + public void beforeSuite() throws RepositoryException { + session = getRepository().login(getCredentials()); + node = session.getRootNode().addNode("testnode", "nt:unstructured"); + session.save(); + } + + @Override + public void beforeTest() throws RepositoryException { + node.setProperty("count", -1); + session.save(); + } + + @Override + public void runTest() throws Exception { + for (int i = 0; i < 1000; i++) { + node.setProperty("count", i); + session.save(); + } + } + + @Override + public void afterTest() throws RepositoryException { + } + + @Override + public void afterSuite() throws RepositoryException { + session.getRootNode().getNode("testnode").remove(); + session.save(); + session.logout(); + } + +} diff --git a/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/SimpleSearchTest.java b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/SimpleSearchTest.java new file mode 100644 index 00000000000..18edc0029aa --- /dev/null +++ b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/SimpleSearchTest.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.performance; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; + +/** + * Run a simple query of the form "//*[@testcount=...]". + */ +public class SimpleSearchTest extends AbstractTest { + + private static final int NODE_COUNT = 100; + + private Session session; + + private Node root; + + protected Query createQuery(QueryManager manager, int i) + throws RepositoryException { + @SuppressWarnings("deprecation") + String xpath = Query.XPATH; + return manager.createQuery("//*[@testcount=" + i + "]", xpath); + } + + @Override + public void beforeSuite() throws RepositoryException { + session = getRepository().login(getCredentials()); + + root = session.getRootNode().addNode("testroot", "nt:unstructured"); + for (int i = 0; i < NODE_COUNT; i++) { + Node node = root.addNode("node" + i, "nt:unstructured"); + for (int j = 0; j < NODE_COUNT; j++) { + Node child = node.addNode("node" + j, "nt:unstructured"); + child.setProperty("testcount", j); + } + session.save(); + } + + IndexManager.createPropertyIndex(session, "testcount"); + + } + + @Override + public void runTest() throws Exception { + QueryManager manager = session.getWorkspace().getQueryManager(); + for (int i = 0; i < NODE_COUNT; i++) { + Query query = createQuery(manager, i); + NodeIterator iterator = query.execute().getNodes(); + while (iterator.hasNext()) { + Node node = iterator.nextNode(); + if (node.getProperty("testcount").getLong() != i) { + throw new Exception("Invalid test result: " + node.getPath()); + } + } + } + } + + @Override + public void afterSuite() throws RepositoryException { + for (int i = 0; i < NODE_COUNT; i++) { + root.getNode("node" + i).remove(); + session.save(); + } + + root.remove(); + session.save(); + session.logout(); + } + +} diff --git a/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/SmallFileReadTest.java b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/SmallFileReadTest.java new file mode 100644 index 00000000000..947cfd05fcf --- /dev/null +++ b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/SmallFileReadTest.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.performance; + +import java.io.InputStream; +import java.util.Calendar; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.output.NullOutputStream; + +public class SmallFileReadTest extends AbstractTest { + + private static final int FILE_COUNT = 1000; + + private static final int FILE_SIZE = 10; + + private Session session; + + private Node root; + + @Override + public void beforeSuite() throws RepositoryException { + session = getRepository().login(getCredentials()); + + root = session.getRootNode().addNode( + "SmallFileReadTest", "nt:folder"); + for (int i = 0; i < FILE_COUNT; i++) { + Node file = root.addNode("file" + i, "nt:file"); + Node content = file.addNode("jcr:content", "nt:resource"); + content.setProperty("jcr:mimeType", "application/octet-stream"); + content.setProperty("jcr:lastModified", Calendar.getInstance()); + content.setProperty( + "jcr:data", new TestInputStream(FILE_SIZE * 1024)); + } + session.save(); + } + + @Override + public void runTest() throws Exception { + for (int i = 0; i < FILE_COUNT; i++) { + Node file = root.getNode("file" + i); + Node content = file.getNode("jcr:content"); + InputStream stream = content.getProperty("jcr:data").getStream(); + try { + IOUtils.copy(stream, new NullOutputStream()); + } finally { + stream.close(); + } + } + } + + @Override + public void afterSuite() throws RepositoryException { + root.remove(); + session.save(); + session.logout(); + } + +} diff --git a/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/SmallFileWriteTest.java b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/SmallFileWriteTest.java new file mode 100644 index 00000000000..73d1770bba0 --- /dev/null +++ b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/SmallFileWriteTest.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.performance; + +import java.util.Calendar; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +public class SmallFileWriteTest extends AbstractTest { + + private static final int FILE_COUNT = 100; + + private static final int FILE_SIZE = 10; + + private Session session; + + private Node root; + + @Override + public void beforeSuite() throws RepositoryException { + session = loginWriter(); + } + + @Override + public void beforeTest() throws RepositoryException { + root = session.getRootNode().addNode("SmallFileWriteTest", "nt:folder"); + session.save(); + } + + @Override + public void runTest() throws Exception { + for (int i = 0; i < FILE_COUNT; i++) { + Node file = root.addNode("file" + i, "nt:file"); + Node content = file.addNode("jcr:content", "nt:resource"); + content.setProperty("jcr:mimeType", "application/octet-stream"); + content.setProperty("jcr:lastModified", Calendar.getInstance()); + content.setProperty( + "jcr:data", new TestInputStream(FILE_SIZE * 1024)); + } + session.save(); + } + + @Override + public void afterTest() throws RepositoryException { + root.remove(); + session.save(); + } + +} diff --git a/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/TestInputStream.java b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/TestInputStream.java new file mode 100644 index 00000000000..18957c0b08b --- /dev/null +++ b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/TestInputStream.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.performance; + +import java.io.InputStream; +import java.util.Random; + +/** + * An input stream that returns a given number of dummy data. The returned + * data is designed to be non-compressible to prevent possible compression + * mechanisms from affecting performance measurements. + */ +class TestInputStream extends InputStream { + + private final int n; + + private int i; + + /** + * Source of the random stream of bytes. No fixed seed is used to + * prevent a solution like the Jackrabbit data store from using just + * a single storage location for multiple streams. + */ + private final Random random = new Random(); + + public TestInputStream(int length) { + n = length; + i = 0; + } + + @Override + public int read() { + if (i < n) { + i++; + byte[] b = new byte[1]; + random.nextBytes(b); + return b[0]; + } else { + return -1; + } + } + + @Override + public int read(byte[] b, int off, int len) { + if (i < n) { + byte[] data = new byte[Math.min(len, n - i)]; + random.nextBytes(data); + System.arraycopy(data, 0, b, off, data.length); + i += data.length; + return data.length; + } else { + return -1; + } + } + + +} diff --git a/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/TransientManyChildNodesTest.java b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/TransientManyChildNodesTest.java new file mode 100644 index 00000000000..ae327efb2a6 --- /dev/null +++ b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/TransientManyChildNodesTest.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.performance; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** + * Test for measuring the performance of {@value #ITERATIONS} iterations of + * transiently adding and removing a child node to a node that already has + * {@value #CHILD_COUNT} existing child nodes. + */ +public class TransientManyChildNodesTest extends AbstractTest { + + private static final int CHILD_COUNT = 10 * 1000; + + private static final int ITERATIONS = 10; + + private Session session; + + private Node node; + + @Override + public void beforeSuite() throws RepositoryException { + session = getRepository().login(getCredentials()); + node = session.getRootNode().addNode("testnode", "nt:unstructured"); + for (int i = 0; i < CHILD_COUNT; i++) { + node.addNode("node" + i, "nt:unstructured"); + } + } + + @Override + public void beforeTest() throws RepositoryException { + } + + @Override + public void runTest() throws Exception { + for (int i = 0; i < ITERATIONS; i++) { + node.addNode("onemore", "nt:unstructured").remove(); + } + } + + @Override + public void afterTest() throws RepositoryException { + } + + @Override + public void afterSuite() throws RepositoryException { + session.getRootNode().getNode("testnode").remove(); + session.save(); + session.logout(); + } + +} diff --git a/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/UpdateManyChildNodesTest.java b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/UpdateManyChildNodesTest.java new file mode 100644 index 00000000000..a306c52e3e4 --- /dev/null +++ b/oak-bench/base/src/main/java/org/apache/jackrabbit/oak/performance/UpdateManyChildNodesTest.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.performance; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** + * Test for measuring the performance of adding one extra child node to + * node with {@value #CHILD_COUNT} existing child nodes. + */ +public class UpdateManyChildNodesTest extends AbstractTest { + + private static final int CHILD_COUNT = 10 * 1000; + + private Session session; + + private Node node; + + @Override + public void beforeSuite() throws RepositoryException { + session = getRepository().login(getCredentials()); + node = session.getRootNode().addNode("testnode", "nt:unstructured"); + for (int i = 0; i < CHILD_COUNT; i++) { + node.addNode("node" + i, "nt:unstructured"); + } + } + + @Override + public void beforeTest() throws RepositoryException { + } + + @Override + public void runTest() throws Exception { + node.addNode("onemore", "nt:unstructured"); + session.save(); + } + + @Override + public void afterTest() throws RepositoryException { + node.getNode("onemore").remove(); + session.save(); + } + + @Override + public void afterSuite() throws RepositoryException { + session.getRootNode().getNode("testnode").remove(); + session.save(); + session.logout(); + } + +} diff --git a/oak-bench/latest/pom.xml b/oak-bench/latest/pom.xml new file mode 100644 index 00000000000..75fb19d4490 --- /dev/null +++ b/oak-bench/latest/pom.xml @@ -0,0 +1,58 @@ + + + + + + 4.0.0 + + + org.apache.jackrabbit + oak-bench-parent + 0.6-SNAPSHOT + ../parent/pom.xml + + + oak-bench-latest + Oak Performance Test + + + true + + + + + org.apache.jackrabbit + oak-bench-base + ${project.version} + test + + + javax.jcr + jcr + 2.0 + test + + + org.apache.jackrabbit + oak-jcr + ${project.version} + test + + + + diff --git a/oak-bench/latest/src/test/java/org/apache/jackrabbit/oak/performance/PerformanceBenchmark.java b/oak-bench/latest/src/test/java/org/apache/jackrabbit/oak/performance/PerformanceBenchmark.java new file mode 100644 index 00000000000..383f8eb2e2f --- /dev/null +++ b/oak-bench/latest/src/test/java/org/apache/jackrabbit/oak/performance/PerformanceBenchmark.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.performance; + +import org.junit.Test; + +public class PerformanceBenchmark extends AbstractPerformanceTest { + + @Test + public void testPerformance() throws Exception { + testPerformance("0.3", "default"); + // testPerformance("0.3", "other_mk"); + } +} diff --git a/oak-bench/parent/pom.xml b/oak-bench/parent/pom.xml new file mode 100644 index 00000000000..9d1c2a0cf29 --- /dev/null +++ b/oak-bench/parent/pom.xml @@ -0,0 +1,119 @@ + + + + + + 4.0.0 + + + + + + + org.apache.jackrabbit + oak-parent + 0.6-SNAPSHOT + ../../oak-parent/pom.xml + + + oak-bench-parent + Oak Performance Test Parent + pom + + + \d\.\d + .* + 0 + + + + + + + maven-surefire-plugin + + -Xms256m -Xmx512m + false + + + repo + ${repo} + + + only + ${only} + + + scale + ${scale} + + + + + + + + + + + profiler + + + agentlib + + + + + + + maven-surefire-plugin + + -Xmx512m -XX:MaxPermSize=512m -agentlib:${agentlib} + + + + + + + + benchmark + + + + maven-failsafe-plugin + 2.12 + + + + integration-test + verify + + + + **/*Benchmark.java + + + + + + + + + + + diff --git a/oak-bench/plot.sh b/oak-bench/plot.sh new file mode 100644 index 00000000000..d69187be503 --- /dev/null +++ b/oak-bench/plot.sh @@ -0,0 +1,61 @@ +#!/bin/sh +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This is an example Gnuplot script for plotting the performance results +# produced by the Jackrabbit performance test suite. Before you run this +# script you need to preprocess the individual performance reports. + +cat <target/report.html + + + Jackrabbit performance + + +

Jackrabbit performance

+

+HTML + +for dat in */target/*.txt; do + cat "$dat" >>target/`basename "$dat"` +done + +for dat in target/*.txt; do + name=`basename "$dat" .txt` + rows=`grep -v "#" "$dat" | wc -l` + gnuplot <>target/report.html + $name +HTML +done + +cat <>target/report.html +

+ + +HTML + +echo file://`pwd`/target/report.html + diff --git a/oak-bench/pom.xml b/oak-bench/pom.xml index d40d53f39d2..2d4b5e0fe4b 100644 --- a/oak-bench/pom.xml +++ b/oak-bench/pom.xml @@ -17,24 +17,32 @@ limitations under the License. --> - + 4.0.0 + + + + org.apache.jackrabbit - oak-parent - 0.1-SNAPSHOT - ../oak-parent/pom.xml + oak-bench-parent + 0.6-SNAPSHOT + parent/pom.xml oak-bench - Oak Benchmark + Oak Performance Benchmark + pom true + + parent + base + latest + + diff --git a/oak-commons/pom.xml b/oak-commons/pom.xml new file mode 100644 index 00000000000..b371616edf2 --- /dev/null +++ b/oak-commons/pom.xml @@ -0,0 +1,69 @@ + + + + + + 4.0.0 + + + org.apache.jackrabbit + oak-parent + 0.6-SNAPSHOT + ../oak-parent/pom.xml + + + oak-commons + Oak Commons + bundle + + + + + org.apache.felix + maven-bundle-plugin + + + + + + + + org.slf4j + slf4j-api + 1.6.4 + + + com.google.code.findbugs + jsr305 + 2.0.0 + + + + + junit + junit + test + + + ch.qos.logback + logback-classic + 1.0.1 + test + + + diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/PathUtils.java b/oak-commons/src/main/java/org/apache/jackrabbit/oak/commons/PathUtils.java similarity index 73% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/util/PathUtils.java rename to oak-commons/src/main/java/org/apache/jackrabbit/oak/commons/PathUtils.java index cc5de50ba17..083deaea0e5 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/PathUtils.java +++ b/oak-commons/src/main/java/org/apache/jackrabbit/oak/commons/PathUtils.java @@ -14,14 +14,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.util; +package org.apache.jackrabbit.oak.commons; -import java.util.ArrayList; import java.util.Iterator; import java.util.NoSuchElementException; +import javax.annotation.Nonnull; + /** - * Utility methods to parse a JCR path. + * Utility methods to parse a path. *

* Each method validates the input, except if the system property * {packageName}.SKIP_VALIDATION is set, in which case only minimal validation @@ -30,15 +31,9 @@ */ public class PathUtils { - /** - * Controls whether paths passed into methods of this class are validated or - * not. By default, paths are validated for each method call, which - * potentially slows down processing. To disable validation, set the system - * property org.apache.jackrabbit.mk.util.PathUtils.SKIP_VALIDATION. - */ - private static final boolean SKIP_VALIDATION = Boolean.getBoolean(PathUtils.class.getName() + ".SKIP_VALIDATION"); - - private static final String[] EMPTY_ARRAY = new String[0]; + private PathUtils() { + // utility class + } /** * Whether the path is the root path ("/"). @@ -47,7 +42,7 @@ public class PathUtils { * @return whether this is the root */ public static boolean denotesRoot(String path) { - assertValid(path); + assert isValid(path); return denotesRootPath(path); } @@ -63,13 +58,13 @@ private static boolean denotesRootPath(String path) { * @return true if it starts with a slash */ public static boolean isAbsolute(String path) { - assertValid(path); + assert isValid(path); return isAbsolutePath(path); } private static boolean isAbsolutePath(String path) { - return path.length() > 0 && path.charAt(0) == '/'; + return !path.isEmpty() && path.charAt(0) == '/'; } /** @@ -79,6 +74,7 @@ private static boolean isAbsolutePath(String path) { * @param path the path * @return the parent path */ + @Nonnull public static String getParentPath(String path) { return getAncestorPath(path, 1); } @@ -92,10 +88,11 @@ public static String getParentPath(String path) { * @param path the path * @return the ancestor path */ + @Nonnull public static String getAncestorPath(String path, int nth) { - assertValid(path); + assert isValid(path); - if (path.length() == 0 || denotesRootPath(path) + if (path.isEmpty() || denotesRootPath(path) || nth <= 0) { return path; } @@ -123,10 +120,11 @@ public static String getAncestorPath(String path, int nth) { * @param path the complete path * @return the last element */ + @Nonnull public static String getName(String path) { - assertValid(path); + assert isValid(path); - if (path.length() == 0 || denotesRootPath(path)) { + if (path.isEmpty() || denotesRootPath(path)) { return ""; } int end = path.length() - 1; @@ -145,7 +143,7 @@ public static String getName(String path) { * @return the number of elements */ public static int getDepth(String path) { - assertValid(path); + assert isValid(path); int count = 1, i = 0; if (isAbsolutePath(path)) { @@ -164,93 +162,56 @@ public static int getDepth(String path) { } /** - * Split a path into elements. The root path ("/") and the empty path ("") - * is zero elements. - * - * @param path the path - * @return the path elements - */ - public static String[] split(String path) { - assertValid(path); - - if (path.length() == 0) { - return EMPTY_ARRAY; - } else if (isAbsolutePath(path)) { - if (path.length() == 1) { - return EMPTY_ARRAY; - } - path = path.substring(1); - } - ArrayList list = new ArrayList(); - while (true) { - int index = path.indexOf('/'); - if (index < 0) { - list.add(path); - break; - } - String s = path.substring(0, index); - list.add(s); - path = path.substring(index + 1); - } - String[] array = new String[list.size()]; - list.toArray(array); - return array; - } - - /** - * Split a path into elements. The root path ("/") and the empty path ("") - * is zero elements. + * Returns an {@code Iterable} for the path elements. The root path ("/") and the + * empty path ("") have zero elements. * * @param path the path * @return an Iterable for the path elements */ + @Nonnull public static Iterable elements(final String path) { - assertValid(path); + assert isValid(path); final Iterator it = new Iterator() { int pos = PathUtils.isAbsolute(path) ? 1 : 0; String next; + @Override public boolean hasNext() { if (next == null) { if (pos >= path.length()) { return false; } - else { - int i = path.indexOf('/', pos); - if (i < 0) { - next = path.substring(pos); - pos = path.length(); - } - else { - next = path.substring(pos, i); - pos = i + 1; - } - return true; + int i = path.indexOf('/', pos); + if (i < 0) { + next = path.substring(pos); + pos = path.length(); + } else { + next = path.substring(pos, i); + pos = i + 1; } } - else { - return true; - } + return true; } + @Override public String next() { if (hasNext()) { String next = this.next; this.next = null; return next; } - else { - throw new NoSuchElementException(); - } + throw new NoSuchElementException(); } + @Override public void remove() { throw new UnsupportedOperationException("remove"); } }; return new Iterable() { + @Override public Iterator iterator() { return it; } @@ -264,20 +225,20 @@ public Iterator iterator() { * @param relativePaths the relative path elements to add * @return the concatenated path */ + @Nonnull public static String concat(String parentPath, String... relativePaths) { - assertValid(parentPath); + assert isValid(parentPath); int parentLen = parentPath.length(); int size = relativePaths.length; StringBuilder buff = new StringBuilder(parentLen + size * 5); buff.append(parentPath); boolean needSlash = parentLen > 0 && !denotesRootPath(parentPath); - for (int i = 0; i < size; i++) { - String s = relativePaths[i]; - assertValid(s); + for (String s : relativePaths) { + assert isValid(s); if (isAbsolutePath(s)) { throw new IllegalArgumentException("Cannot append absolute path " + s); } - if (s.length() > 0) { + if (!s.isEmpty()) { if (needSlash) { buff.append('/'); } @@ -295,13 +256,14 @@ public static String concat(String parentPath, String... relativePaths) { * @param subPath the subPath path to add * @return the concatenated path */ + @Nonnull public static String concat(String parentPath, String subPath) { - assertValid(parentPath); - assertValid(subPath); + assert isValid(parentPath); + assert isValid(subPath); // special cases - if (parentPath.length() == 0) { + if (parentPath.isEmpty()) { return subPath; - } else if (subPath.length() == 0) { + } else if (subPath.isEmpty()) { return parentPath; } else if (isAbsolutePath(subPath)) { throw new IllegalArgumentException("Cannot append absolute path " + subPath); @@ -322,12 +284,16 @@ public static String concat(String parentPath, String subPath) { * @return true if the path is an offspring of the ancestor */ public static boolean isAncestor(String ancestor, String path) { - assertValid(ancestor); - assertValid(path); - if (ancestor.length() == 0 || path.length() == 0) { + assert isValid(ancestor); + assert isValid(path); + if (ancestor.isEmpty() || path.isEmpty()) { return false; } - if (!denotesRoot(ancestor)) { + if (denotesRoot(ancestor)) { + if (denotesRoot(path)) { + return false; + } + } else { ancestor += "/"; } return path.startsWith(ancestor); @@ -335,16 +301,17 @@ public static boolean isAncestor(String ancestor, String path) { /** * Relativize a path wrt. a parent path such that - * relativize(parentPath, concat(parentPath, path)) == paths + * {@code relativize(parentPath, concat(parentPath, path)) == paths} * holds. * * @param parentPath parent pth * @param path path to relativize * @return relativized path */ + @Nonnull public static String relativize(String parentPath, String path) { - assertValid(parentPath); - assertValid(path); + assert isValid(parentPath); + assert isValid(path); if (parentPath.equals(path)) { return ""; @@ -369,7 +336,7 @@ public static String relativize(String parentPath, String path) { * if not found */ public static int getNextSlash(String path, int index) { - assertValid(path); + assert isValid(path); return path.indexOf('/', index); } @@ -384,7 +351,7 @@ public static int getNextSlash(String path, int index) { * @param path the path */ public static void validate(String path) { - if (path.length() == 0 || denotesRootPath(path)) { + if (path.isEmpty() || denotesRootPath(path)) { return; } else if (path.charAt(path.length() - 1) == '/') { throw new IllegalArgumentException("Path may not end with '/': " + path); @@ -401,12 +368,32 @@ public static void validate(String path) { } } - //------------------------------------------< private >--- - - private static void assertValid(String path) { - if (!SKIP_VALIDATION) { - validate(path); + /** + * Check if the path is valid. A valid path is absolute (starts with a '/') + * or relative (doesn't start with '/'), and contains none or more elements. + * A path may not end with '/', except for the root path. Elements itself must + * be at least one character long. + * + * @param path the path + * @return {@code true} iff the path is valid. + */ + public static boolean isValid(String path) { + if (path.isEmpty() || denotesRootPath(path)) { + return true; + } else if (path.charAt(path.length() - 1) == '/') { + return false; + } + char last = 0; + for (int index = 0, len = path.length(); index < len; index++) { + char c = path.charAt(index); + if (c == '/') { + if (last == '/') { + return false; + } + } + last = c; } + return true; } } diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/PathTest.java b/oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/PathTest.java similarity index 65% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/util/PathTest.java rename to oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/PathTest.java index 486d13e87ef..791b3b9f85b 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/PathTest.java +++ b/oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/PathTest.java @@ -14,27 +14,42 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.util; +package org.apache.jackrabbit.oak.commons; +import junit.framework.AssertionFailedError; import junit.framework.TestCase; -import java.util.Iterator; - +/** + * Test the PathUtils class. + */ public class PathTest extends TestCase { + static boolean assertsEnabled; + + static { + assert assertsEnabled = true; + } public void test() { try { PathUtils.getParentPath("invalid/path/"); - fail(); - } catch (IllegalArgumentException e) { + if (assertsEnabled) { + fail(); + } + } catch (AssertionFailedError e) { + throw e; + } catch (AssertionError e) { // expected } try { PathUtils.getName("invalid/path/"); - fail(); - } catch (IllegalArgumentException e) { + if (assertsEnabled) { + fail(); + } + } catch (AssertionFailedError e) { + throw e; + } catch (AssertionError e) { // expected } @@ -42,29 +57,59 @@ public void test() { test("x", "y"); } - private void test(String parent, String child) { + private static int getElementCount(String path) { + int count = 0; + for (String p : PathUtils.elements(path)) { + assertFalse(PathUtils.isAbsolute(p)); + count++; + } + return count; + } + + private static String getElement(String path, int index) { + int count = 0; + for (String p : PathUtils.elements(path)) { + if (index == count++) { + return p; + } + } + fail(); + return ""; + } + + private static void test(String parent, String child) { // split - assertEquals(0, PathUtils.split("").length); - assertEquals(0, PathUtils.split("/").length); - assertEquals(1, PathUtils.split(parent).length); - assertEquals(2, PathUtils.split(parent + "/" + child).length); - assertEquals(1, PathUtils.split("/" + parent).length); - assertEquals(2, PathUtils.split("/" + parent + "/" + child).length); - assertEquals(3, PathUtils.split("/" + parent + "/" + child + "/" + child).length); - assertEquals(parent, PathUtils.split(parent)[0]); - assertEquals(parent, PathUtils.split(parent + "/" + child)[0]); - assertEquals(child, PathUtils.split(parent + "/" + child)[1]); - assertEquals(child, PathUtils.split(parent + "/" + child + "/" + child + "1")[1]); - assertEquals(child + "1", PathUtils.split(parent + "/" + child + "/" + child + "1")[2]); + assertEquals(0, getElementCount("")); + assertEquals(0, getElementCount("/")); + assertEquals(1, getElementCount(parent)); + assertEquals(2, getElementCount(parent + "/" + child)); + assertEquals(1, getElementCount("/" + parent)); + assertEquals(2, getElementCount("/" + parent + "/" + child)); + assertEquals(3, getElementCount("/" + parent + "/" + child + "/" + child)); + assertEquals(parent, getElement(parent, 0)); + assertEquals(parent, getElement(parent + "/" + child, 0)); + assertEquals(child, getElement(parent + "/" + child, 1)); + assertEquals(child, getElement(parent + "/" + child + "/" + child + "1", 1)); + assertEquals(child + "1", getElement(parent + "/" + child + "/" + child + "1", 2)); // concat assertEquals(parent + "/" + child, PathUtils.concat(parent, child)); try { assertEquals(parent + "/" + child, PathUtils.concat(parent + "/", "/" + child)); - fail(); + if (assertsEnabled) { + fail(); + } + } catch (AssertionFailedError e) { + throw e; } catch (IllegalArgumentException e) { - // expected + if (assertsEnabled) { + throw e; + } + } catch (AssertionError e) { + if (!assertsEnabled) { + throw e; + } } try { assertEquals(parent + "/" + child, PathUtils.concat(parent, "/" + child)); @@ -90,13 +135,21 @@ private void test(String parent, String child) { } try { PathUtils.concat("", "//"); - fail(); - } catch (IllegalArgumentException e) { + if (assertsEnabled) { + fail(); + } + } catch (AssertionFailedError e) { + throw e; + } catch (AssertionError e) { // expected } try { PathUtils.concat("/", "/"); - fail(); + if (assertsEnabled) { + fail(); + } + } catch (AssertionFailedError e) { + throw e; } catch (IllegalArgumentException e) { // expected } @@ -143,6 +196,9 @@ private void test(String parent, String child) { assertEquals(false, PathUtils.isAbsolute(parent + "/" + child)); // isAncestor + assertFalse(PathUtils.isAncestor("/", "/")); + assertFalse(PathUtils.isAncestor("/" + parent, "/" + parent)); + assertFalse(PathUtils.isAncestor(parent, parent)); assertTrue(PathUtils.isAncestor("/", "/" + parent)); assertTrue(PathUtils.isAncestor(parent, parent + "/" + child)); assertFalse(PathUtils.isAncestor("/", parent + "/" + child)); @@ -252,102 +308,145 @@ public void testValidateEverything() { String invalid = "/test/test//test/test"; try { PathUtils.denotesRoot(invalid); - fail(); - } catch (IllegalArgumentException e) { + if (assertsEnabled) { + fail(); + } + } catch (AssertionFailedError e) { + throw e; + } catch (AssertionError e) { // expected } try { PathUtils.concat(invalid, "x"); - fail(); - } catch (IllegalArgumentException e) { + if (assertsEnabled) { + fail(); + } + } catch (AssertionFailedError e) { + throw e; + } catch (AssertionError e) { // expected } try { PathUtils.concat("/x", invalid); - fail(); + if (assertsEnabled) { + fail(); + } + } catch (AssertionFailedError e) { + throw e; } catch (IllegalArgumentException e) { - // expected + if (assertsEnabled) { + throw e; + } + } catch (AssertionError e) { + if (!assertsEnabled) { + throw e; + } } try { PathUtils.concat("/x", "y", invalid); - fail(); + if (assertsEnabled) { + fail(); + } + } catch (AssertionFailedError e) { + throw e; } catch (IllegalArgumentException e) { - // expected + if (assertsEnabled) { + throw e; + } + } catch (AssertionError e) { + if (!assertsEnabled) { + throw e; + } } try { PathUtils.concat(invalid, "y", "z"); - fail(); - } catch (IllegalArgumentException e) { + if (assertsEnabled) { + fail(); + } + } catch (AssertionFailedError e) { + throw e; + } catch (AssertionError e) { // expected } try { PathUtils.getDepth(invalid); - fail(); - } catch (IllegalArgumentException e) { + if (assertsEnabled) { + fail(); + } + } catch (AssertionFailedError e) { + throw e; + } catch (AssertionError e) { // expected } try { PathUtils.getName(invalid); - fail(); - } catch (IllegalArgumentException e) { + if (assertsEnabled) { + fail(); + } + } catch (AssertionFailedError e) { + throw e; + } catch (AssertionError e) { // expected } try { PathUtils.getNextSlash(invalid, 0); - fail(); - } catch (IllegalArgumentException e) { + if (assertsEnabled) { + fail(); + } + } catch (AssertionFailedError e) { + throw e; + } catch (AssertionError e) { // expected } try { PathUtils.getParentPath(invalid); - fail(); - } catch (IllegalArgumentException e) { + if (assertsEnabled) { + fail(); + } + } catch (AssertionFailedError e) { + throw e; + } catch (AssertionError e) { // expected } try { PathUtils.isAbsolute(invalid); - fail(); - } catch (IllegalArgumentException e) { + if (assertsEnabled) { + fail(); + } + } catch (AssertionFailedError e) { + throw e; + } catch (AssertionError e) { // expected } try { PathUtils.relativize(invalid, invalid); - fail(); - } catch (IllegalArgumentException e) { + if (assertsEnabled) { + fail(); + } + } catch (AssertionFailedError e) { + throw e; + } catch (AssertionError e) { // expected } try { PathUtils.relativize("/test", invalid); - fail(); - } catch (IllegalArgumentException e) { - // expected - } - try { - PathUtils.split(invalid); - fail(); - } catch (IllegalArgumentException e) { + if (assertsEnabled) { + fail(); + } + } catch (AssertionFailedError e) { + throw e; + } catch (AssertionError e) { // expected } } public void testPathElements() { - String[] paths = new String[]{"", "/", "/a", "a", "/abc/def/ghj", "abc/def/ghj"}; - for (String path : paths) { - String[] elements = PathUtils.split(path); - Iterator it = PathUtils.elements(path).iterator(); - for (String element : elements) { - assertTrue(it.hasNext()); - assertEquals(element, it.next()); - } - assertFalse(it.hasNext()); - } - String[] invalidPaths = new String[]{"//", "/a/", "a/", "/a//", "a//b"}; for (String path: invalidPaths) { try { PathUtils.elements(path); fail(); - } catch (IllegalArgumentException e) { + } catch (AssertionError e) { // expected } } diff --git a/oak-core/README.md b/oak-core/README.md new file mode 100644 index 00000000000..f94fdc92c3e --- /dev/null +++ b/oak-core/README.md @@ -0,0 +1,120 @@ +Oak Core +======== + +Oak API +------- + +The API for accessing core Oak functionality is located in the +`org.apache.jackrabbit.oak.api` package and consists of the following +key interfaces: + + * ContentRepository + * ContentSession + * Root / Tree + +The `ContentRepository` interface represents an entire Oak content repository. +The repository may local or remote, or a cluster of any size. These deployment +details are all hidden behind this interface. + +Starting and stopping `ContentRepository` instances is the responsibility of +each particular deployment and not covered by these interfaces. Repository +clients should use a deployment-specific mechanism (JNDI, OSGi service, etc.) +to acquire references to `ContentRepository` instances. + +All content in the repository is accessed through authenticated sessions +acquired through the `ContentRepository.login()` method. The method takes +explicit access credentials and other login details and, assuming the +credentials are valid, returns a `ContentSession` instance that encapsulates +this information. Session instances are `Closeable` and need to be closed +to release associated resources once no longer used. The recommended access +pattern is: + + ContentRepository repository = ...; + ContentSession session = repository.login(...); + try { + ...; // Use the session + } finally { + session.close(); + } + +All `ContentRepository` and `ContentSession` instances are thread-safe. + +The authenticated `ContentSession` gives you properly authorized access to +the hierarchical content tree inside the repository through instances of the +`Root` and `Tree` interfaces. The `getCurrentRoot()` method returns a +snapshot of the current state of the content tree: + + ContentSession session = ...; + Root root = session.getCurrentRoot(); + Tree tree = root.getTree("/"); + +The returned `Tree` instance belongs to the client and its state is only +modified in response to method calls made by the client. `Tree` instances +are *not* thread-safe for write access, so writing clients need to ensure +that they are not accessed concurrently from multiple threads. `Tree` +instances *are* however thread-safe for read access, so implementations +need to ensure that all reading clients see a coherent state. + +Content trees are recursive data structures that consist of named properties +and subtrees that share the same namespace, but are accessed through separate +methods like outlined below: + + Tree tree = ...; + for (PropertyState property : tree.getProperties()) { + ...; + } + for (Tree subtree : tree.getChildren()) { + ...; + } + +The repository content snapshot exposed by a `Tree` instance may become +invalid over time due to garbage collection of old content, at which point +an outdated snapshot will start throwing `IllegalStateExceptions` to +indicate that the snapshot is no longer available. To access more recent +content, a client should either call `ContentSession.getCurrentRoot()` to +acquire a fresh new content snapshot or use the `refresh()` method to update +a given `Root` to the latest state of the content repository: + + Root root = ...; + root.refresh(); + +In addition to reading repository content, the client can also make +modifications to the content tree. Such content changes remain local to the +particular `Root` instance (and related subtrees) until explicitly committed. +For example, the following code creates and commits a new subtree containing +nothing but a simple property: + + ContentSession session = ...; + Root root = session.getCurrentRoot(); + Tree tree = root.getTree("/"); + Tree subtree = tree.addSubtree("hello"); + subtree.setProperty("message", "Hello, World!"); + root.commit(); + +Even other `Root` instances acquired from the same `ContentSession` won't +see such changes until they've been committed and the other trees refreshed. +This allows a client to track multiple parallel sets of changes with just a +single authenticated session. + +License +------- + +(see the top-level [LICENSE.txt](../LICENSE.txt) for full license details) + +Collective work: Copyright 2012 The Apache Software Foundation. + +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/oak-core/pom.xml b/oak-core/pom.xml index d509544c163..c4e840fbbb0 100644 --- a/oak-core/pom.xml +++ b/oak-core/pom.xml @@ -17,36 +17,137 @@ limitations under the License. --> - + 4.0.0 org.apache.jackrabbit oak-parent - 0.1-SNAPSHOT + 0.6-SNAPSHOT ../oak-parent/pom.xml oak-core Oak Core + bundle + + + + + org.apache.felix + maven-bundle-plugin + + + + org.apache.jackrabbit.oak, + org.apache.jackrabbit.oak.api, + org.apache.jackrabbit.oak.core, + org.apache.jackrabbit.oak.kernel, + org.apache.jackrabbit.oak.util, + org.apache.jackrabbit.oak.namepath, + org.apache.jackrabbit.oak.plugins.value, + org.apache.jackrabbit.oak.plugins.commit, + org.apache.jackrabbit.oak.plugins.identifier, + org.apache.jackrabbit.oak.plugins.index, + org.apache.jackrabbit.oak.plugins.index.lucene, + org.apache.jackrabbit.oak.plugins.index.property, + org.apache.jackrabbit.oak.plugins.memory, + org.apache.jackrabbit.oak.plugins.name, + org.apache.jackrabbit.oak.plugins.nodetype, + org.apache.jackrabbit.oak.plugins.observation, + org.apache.jackrabbit.oak.spi.query, + org.apache.jackrabbit.oak.spi.commit, + org.apache.jackrabbit.oak.spi.lifecycle, + org.apache.jackrabbit.oak.spi.state, + org.apache.jackrabbit.oak.spi.security, + org.apache.jackrabbit.oak.spi.security.authentication, + org.apache.jackrabbit.oak.spi.security.principal, + org.apache.jackrabbit.oak.spi.security.privilege, + org.apache.jackrabbit.oak.spi.security.user, + org.apache.jackrabbit.oak.spi.security.user.action, + org.apache.jackrabbit.oak.spi.security.user.util, + org.apache.jackrabbit.oak.security + + + org.apache.jackrabbit.oak.osgi.Activator + + + + + + org.apache.felix + maven-scr-plugin + + + + + + org.apache.rat + apache-rat-plugin + + + src/test/resources/org/apache/jackrabbit/oak/util/test.json + + + + + + - + - com.h2database - h2 - 1.3.158 - true + org.osgi + org.osgi.core + provided - com.sleepycat - je - 4.0.92 - true + org.osgi + org.osgi.compendium + provided + + + biz.aQute + bndlib + provided + + + org.apache.felix + org.apache.felix.scr.annotations + provided + + + + org.apache.jackrabbit + oak-mk-api + ${project.version} + + + org.apache.jackrabbit + oak-mk + ${project.version} + + + + org.apache.jackrabbit + oak-mk-remote + ${project.version} + + + + org.apache.jackrabbit + oak-commons + ${project.version} + + + + com.google.guava + guava + ${guava.version} + + + org.mongodb mongo-java-driver @@ -54,18 +155,81 @@ true - + - com.googlecode.json-simple - json-simple - 1.1 - test + javax.jcr + jcr + 2.0 + + + org.apache.jackrabbit + jackrabbit-api + ${jackrabbit.version} + + org.apache.jackrabbit + jackrabbit-jcr-commons + ${jackrabbit.version} + + + + + org.apache.lucene + lucene-core + 4.0.0-BETA + true + + + org.apache.lucene + lucene-analyzers-common + 4.0.0-BETA + true + + + org.apache.tika + tika-core + 1.2 + true + + + + + org.slf4j + slf4j-api + 1.6.4 + + + + + com.google.code.findbugs + jsr305 + 2.0.0 + provided + + + junit junit test + + org.apache.jackrabbit + oak-it-mk + ${project.version} + test + + + com.h2database + h2 + 1.3.158 + test + + + ch.qos.logback + logback-classic + 1.0.1 + test + - diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/MicroKernelFactory.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/MicroKernelFactory.java deleted file mode 100644 index 5629032f695..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/MicroKernelFactory.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk; - -import java.io.IOException; - -import org.apache.jackrabbit.mk.api.MicroKernel; -import org.apache.jackrabbit.mk.client.Client; -import org.apache.jackrabbit.mk.fs.FileUtils; -import org.apache.jackrabbit.mk.simple.SimpleKernelImpl; -import org.apache.jackrabbit.mk.util.ExceptionFactory; -import org.apache.jackrabbit.mk.wrapper.IndexWrapper; -import org.apache.jackrabbit.mk.wrapper.LogWrapper; -import org.apache.jackrabbit.mk.wrapper.SecurityWrapper; -import org.apache.jackrabbit.mk.wrapper.VirtualRepositoryWrapper; - -/** - * A factory to create a MicroKernel instance. - */ -public class MicroKernelFactory { - - /** - * Get an instance. Supported URLs: - *

    - *
  • fs:target/mk-test (using the directory ./target/mk-test)
  • - *
  • fs:target/mk-test;clean (same, but delete the old repository first)
  • - *
  • fs:{homeDir} (use the system property homeDir or '.' if not set)
  • - *
  • simple: (in-memory implementation)
  • - *
  • simple:fs:target/temp (using the directory ./target/temp)
  • - *
- * - * @param url the repository URL - * @return a new instance - */ - public static MicroKernel getInstance(String url) { - if (url.startsWith("mem:")) { - return SimpleKernelImpl.get(url); - } else if (url.startsWith("simple:")) { - return SimpleKernelImpl.get(url); - } else if (url.startsWith("log:")) { - return LogWrapper.get(url); - } else if (url.startsWith("sec:")) { - return SecurityWrapper.get(url); - } else if (url.startsWith("virtual:")) { - return VirtualRepositoryWrapper.get(url); - } else if (url.startsWith("index:")) { - return IndexWrapper.get(url); - } else if (url.startsWith("fs:")) { - boolean clean = false; - if (url.endsWith(";clean")) { - url = url.substring(0, url.length() - ";clean".length()); - clean = true; - } - String dir = url.substring("fs:".length()); - dir = dir.replaceAll("\\{homeDir\\}", System.getProperty("homeDir", ".")); - if (clean) { - try { - FileUtils.deleteRecursive(dir + "/" + ".mk", false); - } catch (IOException e) { - throw ExceptionFactory.convert(e); - } - } - return new MicroKernelImpl(dir); - } else if (url.startsWith("http:")) { - return Client.createHttpClient(url); - } else if (url.startsWith("http-bridge:")) { - MicroKernel mk = MicroKernelFactory.getInstance(url.substring("http-bridge:".length())); - return Client.createHttpBridge(mk); - } else { - throw new IllegalArgumentException(url); - } - } - -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/MicroKernelImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/MicroKernelImpl.java deleted file mode 100644 index b079e0ea4bf..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/MicroKernelImpl.java +++ /dev/null @@ -1,657 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk; - -import org.apache.jackrabbit.mk.api.MicroKernel; -import org.apache.jackrabbit.mk.api.MicroKernelException; -import org.apache.jackrabbit.mk.json.JsopBuilder; -import org.apache.jackrabbit.mk.json.JsopTokenizer; -import org.apache.jackrabbit.mk.model.Commit; -import org.apache.jackrabbit.mk.model.CommitBuilder; -import org.apache.jackrabbit.mk.model.Id; -import org.apache.jackrabbit.mk.model.StoredCommit; -import org.apache.jackrabbit.mk.model.TraversingNodeDiffHandler; -import org.apache.jackrabbit.mk.store.NotFoundException; -import org.apache.jackrabbit.mk.store.RevisionProvider; -import org.apache.jackrabbit.mk.util.CommitGate; -import org.apache.jackrabbit.mk.util.PathUtils; -import org.apache.jackrabbit.mk.util.SimpleLRUCache; -import org.apache.jackrabbit.oak.model.NodeState; -import org.apache.jackrabbit.oak.model.PropertyState; - -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -/** - * - */ -public class MicroKernelImpl implements MicroKernel { - - protected Repository rep; - private final CommitGate gate = new CommitGate(); - - /** - * Key: revision id, Value: diff string - */ - private final Map diffCache = Collections.synchronizedMap(SimpleLRUCache.newInstance(100)); - - public MicroKernelImpl(String homeDir) throws MicroKernelException { - init(homeDir); - } - - /** - * Alternate constructor, used for testing. - * - * @param rep repository, already initialized - */ - public MicroKernelImpl(Repository rep) { - this.rep = rep; - } - - protected void init(String homeDir) throws MicroKernelException { - try { - rep = new Repository(homeDir); - rep.init(); - } catch (Exception e) { - throw new MicroKernelException(e); - } - } - - public void dispose() { - gate.commit("end"); - if (rep != null) { - try { - rep.shutDown(); - } catch (Exception ignore) { - // fail silently - } - rep = null; - } - diffCache.clear(); - } - - public String getHeadRevision() throws MicroKernelException { - if (rep == null) { - throw new IllegalStateException("this instance has already been disposed"); - } - return getHeadRevisionId().toString(); - } - - /** - * Same as getHeadRevisionId, with typed Id return value instead of string. - * - * @see #getHeadRevision() - */ - private Id getHeadRevisionId() throws MicroKernelException { - try { - return rep.getHeadRevision(); - } catch (Exception e) { - throw new MicroKernelException(e); - } - } - - public String getRevisions(long since, int maxEntries) throws MicroKernelException { - if (rep == null) { - throw new IllegalStateException("this instance has already been disposed"); - } - maxEntries = maxEntries < 0 ? Integer.MAX_VALUE : maxEntries; - List history = new ArrayList(); - try { - StoredCommit commit = rep.getHeadCommit(); - while (commit != null - && history.size() < maxEntries - && commit.getCommitTS() >= since) { - history.add(commit); - - Id commitId = commit.getParentId(); - if (commitId == null) { - break; - } - commit = rep.getCommit(commitId); - } - } catch (Exception e) { - throw new MicroKernelException(e); - } - - JsopBuilder buff = new JsopBuilder().array(); - for (int i = history.size() - 1; i >= 0; i--) { - StoredCommit commit = history.get(i); - buff.object(). - key("id").value(commit.getId().toString()). - key("ts").value(commit.getCommitTS()). - endObject(); - } - return buff.endArray().toString(); - } - - public String waitForCommit(String oldHeadRevision, long maxWaitMillis) throws MicroKernelException, InterruptedException { - return gate.waitForCommit(oldHeadRevision, maxWaitMillis); - } - - public String getJournal(String fromRevision, String toRevision, String filter) throws MicroKernelException { - if (rep == null) { - throw new IllegalStateException("this instance has already been disposed"); - } - - Id fromRevisionId = Id.fromString(fromRevision); - Id toRevisionId = toRevision == null ? getHeadRevisionId() : Id.fromString(toRevision); - - List commits = new ArrayList(); - try { - StoredCommit toCommit = rep.getCommit(toRevisionId); - - Commit fromCommit; - if (toRevisionId.equals(fromRevisionId)) { - fromCommit = toCommit; - } else { - fromCommit = rep.getCommit(fromRevisionId); - if (fromCommit.getCommitTS() > toCommit.getCommitTS()) { - // negative range, return empty array - return "[]"; - } - } - - // collect commits, starting with toRevisionId - // and traversing parent commit links until we've reached - // fromRevisionId - StoredCommit commit = toCommit; - while (commit != null) { - commits.add(commit); - if (commit.getId().equals(fromRevisionId)) { - break; - } - Id commitId = commit.getParentId(); - if (commitId == null) { - break; - } - commit = rep.getCommit(commitId); - } - } catch (Exception e) { - throw new MicroKernelException(e); - } - - JsopBuilder commitBuff = new JsopBuilder().array(); - // iterate over commits in chronological order, - // starting with oldest commit - for (int i = commits.size() - 1; i >= 0; i--) { - StoredCommit commit = commits.get(i); - if (commit.getParentId() == null) { - continue; - } - commitBuff.object(). - key("id").value(commit.getId().toString()). - key("ts").value(commit.getCommitTS()). - key("msg").value(commit.getMsg()); - String diff = diffCache.get(commit.getId()); - if (diff == null) { - diff = diff(commit.getParentId(), commit.getId(), filter); - diffCache.put(commit.getId(), diff); - } - commitBuff.key("changes").value(diff).endObject(); - } - return commitBuff.endArray().toString(); - } - - public String diff(String fromRevision, String toRevision, String filter) throws MicroKernelException { - Id toRevisionId = toRevision == null ? getHeadRevisionId() : Id.fromString(toRevision); - - return diff(Id.fromString(fromRevision), toRevisionId, filter); - } - - /** - * Same as diff, with typed Id arguments instead of strings. - * - * @see #diff(String, String, String) - */ - private String diff(Id fromRevisionId, Id toRevisionId, String filter) throws MicroKernelException { - // TODO extract and evaluate filter criteria (such as e.g. 'path') specified in 'filter' parameter - String path = "/"; - - try { - final JsopBuilder buff = new JsopBuilder(); - final RevisionProvider rp = rep.getRevisionStore(); - // maps (key: id of target node, value: path/to/target) - // for tracking added/removed nodes; this allows us - // to detect 'move' operations - final HashMap addedNodes = new HashMap(); - final HashMap removedNodes = new HashMap(); - NodeState node1, node2; - try { - node1 = rep.getNodeState(fromRevisionId, path); - } catch (NotFoundException e) { - node1 = null; - } - try { - node2 = rep.getNodeState(toRevisionId, path); - } catch (NotFoundException e) { - node2 = null; - } - - if (node1 == null) { - if (node2 != null) { - buff.tag('+').key(path).object(); - toJson(buff, node2, Integer.MAX_VALUE, 0, -1, false); - return buff.endObject().newline().toString(); - } else { - throw new MicroKernelException("path doesn't exist in the specified revisions: " + path); - } - } else if (node2 == null) { - buff.tag('-'); - buff.value(path); - return buff.newline().toString(); - } - - TraversingNodeDiffHandler diffHandler = new TraversingNodeDiffHandler() { - @Override - public void propertyAdded(PropertyState after) { - buff.tag('+'). - key(PathUtils.concat(getCurrentPath(), after.getName())). - encodedValue(after.getEncodedValue()). - newline(); - } - - @Override - public void propertyChanged(PropertyState before, PropertyState after) { - buff.tag('^'). - key(PathUtils.concat(getCurrentPath(), after.getName())). - encodedValue(after.getEncodedValue()). - newline(); - } - - @Override - public void propertyDeleted(PropertyState before) { - // since property and node deletions can't be distinguished - // using the "- " notation we're representing - // property deletions as "^ :null" - buff.tag('^'). - key(PathUtils.concat(getCurrentPath(), before.getName())). - value(null). - newline(); - } - - @Override - public void childNodeAdded(String name, NodeState after) { - addedNodes.put(rp.getId(after), PathUtils.concat(getCurrentPath(), name)); - buff.tag('+'). - key(PathUtils.concat(getCurrentPath(), name)).object(); - toJson(buff, after, Integer.MAX_VALUE, 0, -1, false); - buff.endObject().newline(); - } - - @Override - public void childNodeDeleted(String name, NodeState before) { - removedNodes.put(rp.getId(before), PathUtils.concat(getCurrentPath(), name)); - buff.tag('-'); - buff.value(PathUtils.concat(getCurrentPath(), name)); - buff.newline(); - } - }; - diffHandler.start(node1, node2, path); - - // check if this commit includes 'move' operations - // by building intersection of added and removed nodes - addedNodes.keySet().retainAll(removedNodes.keySet()); - if (!addedNodes.isEmpty()) { - // this commit includes 'move' operations - removedNodes.keySet().retainAll(addedNodes.keySet()); - // addedNodes & removedNodes now only contain information about moved nodes - - // re-build the diff in a 2nd pass, this time representing moves correctly - buff.resetWriter(); - - // TODO refactor code, avoid duplication - - diffHandler = new TraversingNodeDiffHandler() { - @Override - public void propertyAdded(PropertyState after) { - buff.tag('+'). - key(PathUtils.concat(getCurrentPath(), after.getName())). - encodedValue(after.getEncodedValue()). - newline(); - } - - @Override - public void propertyChanged(PropertyState before, PropertyState after) { - buff.tag('^'). - key(PathUtils.concat(getCurrentPath(), after.getName())). - encodedValue(after.getEncodedValue()). - newline(); - } - - @Override - public void propertyDeleted(PropertyState before) { - // since property and node deletions can't be distinguished - // using the "- " notation we're representing - // property deletions as "^ :null" - buff.tag('^'). - key(PathUtils.concat(getCurrentPath(), before.getName())). - value(null). - newline(); - } - - @Override - public void childNodeAdded(String name, NodeState after) { - if (addedNodes.containsKey(rp.getId(after))) { - // moved node, will be processed separately - return; - } - buff.tag('+'). - key(PathUtils.concat(getCurrentPath(), name)).object(); - toJson(buff, after, Integer.MAX_VALUE, 0, -1, false); - buff.endObject().newline(); - } - - @Override - public void childNodeDeleted(String name, NodeState before) { - if (addedNodes.containsKey(rp.getId(before))) { - // moved node, will be processed separately - return; - } - buff.tag('-'); - buff.value(PathUtils.concat(getCurrentPath(), name)); - buff.newline(); - } - - }; - diffHandler.start(node1, node2, path); - - // finally process moved nodes - for (Map.Entry entry : addedNodes.entrySet()) { - buff.tag('>'). - // path/to/deleted/node - key(removedNodes.get(entry.getKey())). - // path/to/added/node - value(entry.getValue()). - newline(); - } - } - return buff.toString(); - - } catch (Exception e) { - throw new MicroKernelException(e); - } - } - - public boolean nodeExists(String path, String revision) throws MicroKernelException { - if (rep == null) { - throw new IllegalStateException("this instance has already been disposed"); - } - - Id revisionId = revision == null ? getHeadRevisionId() : Id.fromString(revision); - return rep.nodeExists(revisionId, path); - } - - public long getChildNodeCount(String path, String revision) throws MicroKernelException { - if (rep == null) { - throw new IllegalStateException("this instance has already been disposed"); - } - - Id revisionId = revision == null ? getHeadRevisionId() : Id.fromString(revision); - - try { - return rep.getNodeState(revisionId, path).getChildNodeCount(); - } catch (Exception e) { - throw new MicroKernelException(e); - } - } - - public String getNodes(String path, String revision) throws MicroKernelException { - return getNodes(path, revision, 1, 0, -1, null); - } - - public String getNodes(String path, String revision, int depth, long offset, int count, String filter) throws MicroKernelException { - if (rep == null) { - throw new IllegalStateException("this instance has already been disposed"); - } - - Id revisionId = revision == null ? getHeadRevisionId() : Id.fromString(revision); - - // TODO extract and evaluate filter criteria (such as e.g. ':hash') specified in 'filter' parameter - - try { - JsopBuilder buf = new JsopBuilder().object(); - toJson(buf, rep.getNodeState(revisionId, path), depth, (int) offset, count, true); - return buf.endObject().toString(); - } catch (Exception e) { - throw new MicroKernelException(e); - } - } - - public String commit(String path, String jsonDiff, String revision, String message) throws MicroKernelException { - if (rep == null) { - throw new IllegalStateException("this instance has already been disposed"); - } - if (path.length() > 0 && !PathUtils.isAbsolute(path)) { - throw new IllegalArgumentException("absolute path expected: " + path); - } - - Id revisionId = revision == null ? getHeadRevisionId() : Id.fromString(revision); - - try { - JsopTokenizer t = new JsopTokenizer(jsonDiff); - CommitBuilder cb = rep.getCommitBuilder(revisionId, message); - while (true) { - int r = t.read(); - if (r == JsopTokenizer.END) { - break; - } - int pos; // used for error reporting - switch (r) { - case '+': { - pos = t.getLastPos(); - String subPath = t.readString(); - t.read(':'); - if (t.matches('{')) { - String nodePath = PathUtils.concat(path, subPath); - if (!PathUtils.isAbsolute(nodePath)) { - throw new Exception("absolute path expected: " + nodePath + ", pos: " + pos); - } - String parentPath = PathUtils.getParentPath(nodePath); - String nodeName = PathUtils.getName(nodePath); - // build the list of added nodes recursively - LinkedList list = new LinkedList(); - addNode(list, parentPath, nodeName, t); - for (AddNodeOperation op : list) { - cb.addNode(op.path, op.name, op.props); - } - } else { - String value; - if (t.matches(JsopTokenizer.NULL)) { - value = null; - } else { - value = t.readRawValue().trim(); - } - String targetPath = PathUtils.concat(path, subPath); - if (!PathUtils.isAbsolute(targetPath)) { - throw new Exception("absolute path expected: " + targetPath + ", pos: " + pos); - } - String parentPath = PathUtils.getParentPath(targetPath); - String propName = PathUtils.getName(targetPath); - cb.setProperty(parentPath, propName, value); - } - break; - } - case '-': { - pos = t.getLastPos(); - String subPath = t.readString(); - String targetPath = PathUtils.concat(path, subPath); - if (!PathUtils.isAbsolute(targetPath)) { - throw new Exception("absolute path expected: " + targetPath + ", pos: " + pos); - } - cb.removeNode(targetPath); - break; - } - case '^': { - pos = t.getLastPos(); - String subPath = t.readString(); - t.read(':'); - String value; - if (t.matches(JsopTokenizer.NULL)) { - value = null; - } else { - value = t.readRawValue().trim(); - } - String targetPath = PathUtils.concat(path, subPath); - if (!PathUtils.isAbsolute(targetPath)) { - throw new Exception("absolute path expected: " + targetPath + ", pos: " + pos); - } - String parentPath = PathUtils.getParentPath(targetPath); - String propName = PathUtils.getName(targetPath); - cb.setProperty(parentPath, propName, value); - break; - } - case '>': { - pos = t.getLastPos(); - String subPath = t.readString(); - String srcPath = PathUtils.concat(path, subPath); - if (!PathUtils.isAbsolute(srcPath)) { - throw new Exception("absolute path expected: " + srcPath + ", pos: " + pos); - } - t.read(':'); - pos = t.getLastPos(); - String targetPath = t.readString(); - if (!PathUtils.isAbsolute(targetPath)) { - targetPath = PathUtils.concat(path, targetPath); - if (!PathUtils.isAbsolute(targetPath)) { - throw new Exception("absolute path expected: " + targetPath + ", pos: " + pos); - } - } - cb.moveNode(srcPath, targetPath); - break; - } - case '*': { - pos = t.getLastPos(); - String subPath = t.readString(); - String srcPath = PathUtils.concat(path, subPath); - if (!PathUtils.isAbsolute(srcPath)) { - throw new Exception("absolute path expected: " + srcPath + ", pos: " + pos); - } - t.read(':'); - pos = t.getLastPos(); - String targetPath = t.readString(); - if (!PathUtils.isAbsolute(targetPath)) { - targetPath = PathUtils.concat(path, targetPath); - if (!PathUtils.isAbsolute(targetPath)) { - throw new Exception("absolute path expected: " + targetPath + ", pos: " + pos); - } - } - cb.copyNode(srcPath, targetPath); - break; - } - default: - throw new AssertionError("token type: " + t.getTokenType()); - } - } - Id newHead = cb.doCommit(); - if (!newHead.equals(revisionId)) { - // non-empty commit - gate.commit(newHead.toString()); - } - return newHead.toString(); - } catch (Exception e) { - throw new MicroKernelException(e); - } - } - - public long getLength(String blobId) throws MicroKernelException { - if (rep == null) { - throw new IllegalStateException("this instance has already been disposed"); - } - try { - return rep.getRevisionStore().getBlobLength(blobId); - } catch (Exception e) { - throw new MicroKernelException(e); - } - } - - public int read(String blobId, long pos, byte[] buff, int off, int length) throws MicroKernelException { - if (rep == null) { - throw new IllegalStateException("this instance has already been disposed"); - } - try { - return rep.getRevisionStore().getBlob(blobId, pos, buff, off, length); - } catch (Exception e) { - throw new MicroKernelException(e); - } - } - - public String write(InputStream in) throws MicroKernelException { - if (rep == null) { - throw new IllegalStateException("this instance has already been disposed"); - } - try { - return rep.getRevisionStore().putBlob(in); - } catch (Exception e) { - throw new MicroKernelException(e); - } - } - - //-------------------------------------------------------< implementation > - - void toJson(JsopBuilder builder, NodeState node, int depth, int offset, int count, boolean inclVirtualProps) { - for (PropertyState property : node.getProperties()) { - builder.key(property.getName()).encodedValue(property.getEncodedValue()); - } - long childCount = node.getChildNodeCount(); - if (inclVirtualProps) { - builder.key(":childNodeCount").value(childCount); - } - if (childCount > 0 && depth >= 0) { - // TODO: Use an import once the conflict with .mk.model is resolved - for (org.apache.jackrabbit.oak.model.ChildNodeEntry entry - : node.getChildNodeEntries(offset, count)) { - builder.key(entry.getName()).object(); - if (depth > 0) { - toJson(builder, entry.getNode(), depth - 1, 0, -1, inclVirtualProps); - } - builder.endObject(); - } - } - } - - static void addNode(LinkedList list, String path, String name, JsopTokenizer t) throws Exception { - AddNodeOperation op = new AddNodeOperation(); - op.path = path; - op.name = name; - list.add(op); - if (!t.matches('}')) { - do { - String key = t.readString(); - t.read(':'); - if (t.matches('{')) { - addNode(list, PathUtils.concat(path, name), key, t); - } else { - op.props.put(key, t.readRawValue().trim()); - } - } while (t.matches(',')); - t.read('}'); - } - } - - //--------------------------------------------------------< inner classes > - static class AddNodeOperation { - String path; - String name; - Map props = new HashMap(); - } - -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/api/MicroKernel.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/api/MicroKernel.java deleted file mode 100644 index b78bae053f1..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/api/MicroKernel.java +++ /dev/null @@ -1,324 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.api; - -import java.io.InputStream; - -/** - * The MicroKernel design goals/principles: - *
    - *
  • manage huge trees of nodes and properties efficiently
  • - *
  • MVCC-based concurrency control
  • - *
  • GIT/SVN-inspired DAG-based data model
  • - *
  • highly scalable concurrent read & write operations
  • - *
  • stateless API
  • - *
  • portable to C
  • - *
  • efficient support for large number of child nodes
  • - *
  • integrated API for storing/retrieving large binaries (similar to existing DataStore API)
  • - *
  • human-readable data serialization (JSON)
  • - *
- *

- * The MicroKernel Data Model: - *

    - *
  • simple JSON-inspired data model: just nodes and properties
  • - *
  • a node consists of an unordered set of name -> item mappings. each - * property and child node is uniquely named and a single name can only - * refer to a property or a child node, not both at the same time. - *
  • properties are represented as name/value pairs
  • - *
  • supported property types: string, number
  • - *
  • other property types (weak/hard reference, date, etc) would need to be - * encoded/mangled in name or value
  • - *
  • no support for JCR/XML-like namespaces, "foo:bar" is just an ordinary name
  • - *
- *

- * Architecture (overview): - *

    - *
  1. JCR (full TCK-compliant implementation)
  2. - *
  3. SPI (node types, workspaces, namespaces, access control, search, locking, ...)
  4. - *
  5. MicroKernel
  6. - *
- */ -public interface MicroKernel { - - /** - * Dispose this instance. - */ - void dispose(); - - //---------------------------------------------------------< REVISION ops > - - /** - * Return the id of the current head revision. - * - * @return id of head revision - * @throws MicroKernelException if an error occurs - */ - String getHeadRevision() throws MicroKernelException; - - /** - * Returns a chronological list of all revisions since a specific point - * in time. - *

- * Format: - *

-     * [ { "id" : "", "ts" :  }, ... ]
-     * 
- * - * @param since timestamp (ms) of earliest revision to be returned - * @param maxEntries maximum #entries to be returned; - * if < 0, no limit will be applied. - * @return a chronological list of revisions in JSON format. - * @throws MicroKernelException if an error occurs - */ - String /* jsonArray */ getRevisions(long since, int maxEntries) - throws MicroKernelException; - - /** - * Wait for a commit to occur that is newer than the given revision number. - *

- * This method is useful efficient polling. The method will return the current head revision - * if it is newer than the given old revision number, or wait until the given number of - * milliseconds passed or a new head revision is available. - * - * @param maxWaitMillis the maximum number of milliseconds to wait (0 if the - * method should not wait). - * @return the current head revision - * @throws MicroKernelException if an error occurs - * @throws InterruptedException if the thread was interrupted - */ - String waitForCommit(String oldHeadRevision, long maxWaitMillis) throws MicroKernelException, InterruptedException; - - /** - * Returns a revision journal, starting with fromRevisionId - * and ending with toRevisionId. - *

- * Format: - *

-     * [ { "id" : "<revisionId>", "ts" : "<revisionTimestamp>", "msg" : "<commitMessage>", "changes" : "<JSON diff>" }, ... ]
-     * 
- * - * @param fromRevisionId first revision to be returned in journal - * @param toRevisionId last revision to be returned in journal, if null the current head revision is assumed - * @param filter (optional) filter criteria - * (e.g. path, property names, etc); - * TODO specify format and semantics - * @return a chronological list of revisions in JSON format - * @throws MicroKernelException if an error occurs - */ - String /* jsonArray */ getJournal(String fromRevisionId, String toRevisionId, - String filter) - throws MicroKernelException; - - /** - * Returns the JSON diff representation of the changes between the specified - * revisions. The changes will be consolidated if the specified range - * covers intermediary revisions. The revisions need not be in a specified - * chronological order. - * - *

- * Format: - *

-     * [ { "id" : "<revisionId>", "ts" : "<revisionTimestamp>", "msg" : "<commitMessage>", "changes" : "<JSON diff>" }, ... ]
-     * 
- * - * @param fromRevisionId a revision - * @param toRevisionId another revision, if null the current head revision is assumed - * @param filter (optional) filter criteria - * (e.g. path, property names, etc); - * TODO specify format and semantics - * @return JSON diff representation of the changes - * @throws MicroKernelException if an error occurs - */ - String /* JSON diff */ diff(String fromRevisionId, String toRevisionId, - String filter) - throws MicroKernelException; - - //-------------------------------------------------------------< READ ops > - - /** - * Determines whether the specified node exists. - * - * @param path path denoting node - * @param revisionId revision, if null the current head revision is assumed - * @return true if the specified node exists, otherwise false - * @throws MicroKernelException if an error occurs - */ - boolean nodeExists(String path, String revisionId) throws MicroKernelException; - - /** - * Returns the number of child nodes of the specified node. - *

- * This is a convenience method since this information could gathered by - * calling getNodes(path, revisionId, 0, 0, 0) and evaluating - * the :childNodeCount property. - * - * - * @param path path denoting node - * @param revisionId revision, if null the current head revision is assumed - * @return the number of child nodes - * @throws MicroKernelException if an error occurs - */ - long getChildNodeCount(String path, String revisionId) throws MicroKernelException; - - /** - * Returns the node tree rooted at the specified parent node with depth 1. - * Depth 1 means all properties of the node are returned, including the list - * of child nodes and their properties (including - * :childNodeCount). Example: - *

-     * {
-     *     "someprop": "someval",
-     *     ":childNodeCount": 2,
-     *     "child1" : {
-     *          "prop1": "foo",
-     *          ":childNodeCount": 2
-     *      },
-     *      "child2": {
-     *          "prop1": "bar"
-     *          ":childNodeCount": 0
-     *      }
-     * }
-     * 
- * The collection of name/value pairs denoting child nodes is assumed to be - * ordered. - *

- * Remarks: - *

    - *
  • If the property :childNodeCount equals 0, then the - * node does not have any child nodes. - *
  • If the value of :childNodeCount is larger than the list - * of returned child nodes, then the node has more child nodes than those - * included in the tree. Large number of child nodes can be retrieved in - * chunks using {@link #getNodes(String, String, int, long, int, String)}
  • - *
- * This method is a convenience method for - * getNodes(path, revisionId, 1, 0, -1, null) - * - * @param path path denoting root of node tree to be retrieved - * @param revisionId revision, if null the current head revision is assumed - * @return node tree in JSON format - * @throws MicroKernelException if an error occurs - */ - String /* jsonTree */ getNodes(String path, String revisionId) throws MicroKernelException; - - /** - * Returns the node tree rooted at the specified parent node with the - * specified depth, maximum child node count and offset. The depth of the - * returned tree is governed by the depth parameter: - * - * - * - * - * - * - * - * - * - * - * - * - * - *
depth = 0properties, including :childNodeCount and the list - * of child node names (as empty objects)
depth = 1properties, child nodes and their properties (including - * :childNodeCount)
depth = 2[and so on...]
- * Offset and count only affect the returned child node list of this node. - * - * @param path path denoting root of node tree to be retrieved - * @param revisionId revision, if null the current head revision is assumed - * @param depth maximum depth of returned tree - * @param offset start position in child node list (0 to start at the - * beginning) - * @param count maximum number of child nodes to retrieve (-1 for as many as - * possible) - * @param filter (optional) filter criteria - * (e.g. names of properties to be included, etc); - * TODO specify format and semantics - * @return node tree in JSON format - * @throws MicroKernelException if an error occurs - */ - String /* jsonTree */ getNodes(String path, String revisionId, int depth, long offset, int count, String filter) throws MicroKernelException; - - //------------------------------------------------------------< WRITE ops > - - /** - * Applies the specified changes on the specified target node. - *

- * If path.length() == 0 the paths specified in the - * jsonDiff are expected to be absolute. - *

- * The implementation tries to merge changes if the revision id of the - * commit is set accordingly. As an example, deleting a node is allowed if - * the node existed in the given revision, even if it was deleted in the - * meantime. - * - * @param path path denoting target node - * @param jsonDiff changes to be applied in JSON diff format. - * @param revisionId revision the changes are based on, if null the current head revision is assumed - * @param message commit message - * @return id of newly created revision - * @throws MicroKernelException if an error occurs - */ - String /* revisionId */ commit(String path, String jsonDiff, String revisionId, String message) - throws MicroKernelException; - - - //--------------------------------------------------< BLOB READ/WRITE ops > - - /** - * Returns the length of the specified blob. - * - * @param blobId blob identifier - * @return length of the specified blob - * @throws MicroKernelException if an error occurs - */ - long getLength(String blobId) throws MicroKernelException; - - /** - * Reads up to length bytes of data from the specified blob into - * the given array of bytes. An attempt is made to read as many as - * length bytes, but a smaller number may be read. - * The number of bytes actually read is returned as an integer. - * - * @param blobId blob identifier - * @param pos the offset within the blob - * @param buff the buffer into which the data is read. - * @param off the start offset in array buff - * at which the data is written. - * @param length the maximum number of bytes to read - * @return the total number of bytes read into the buffer, or - * -1 if there is no more data because the end of - * the blob content has been reached. - * @throws MicroKernelException if an error occurs - */ - int /* count */ read(String blobId, long pos, byte[] buff, int off, int length) - throws MicroKernelException; - - /** - * Stores the content of the given stream and returns an associated - * identifier for later retrieval. - *

- * If identical stream content has been stored previously, then the existing - * identifier will be returned instead of storing a redundant copy. - *

- * The stream is closed by this method. - * - * @param in InputStream providing the blob content - * @return blob identifier associated with the given content - * @throws MicroKernelException if an error occurs - */ - String /* blobId */ write(InputStream in) throws MicroKernelException; -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/FileBlobStore.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/FileBlobStore.java deleted file mode 100644 index 552e238d7a7..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/FileBlobStore.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.blobs; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.security.DigestInputStream; -import java.security.MessageDigest; -import org.apache.jackrabbit.mk.fs.FilePath; -import org.apache.jackrabbit.mk.fs.FileUtils; -import org.apache.jackrabbit.mk.util.ExceptionFactory; -import org.apache.jackrabbit.mk.util.IOUtils; -import org.apache.jackrabbit.mk.util.StringUtils; - -/** - * A file blob store. - */ -public class FileBlobStore extends AbstractBlobStore { - - private static final String OLD_SUFFIX = "_old"; - - private final FilePath baseDir; - private final byte[] buffer = new byte[16 * 1024]; - private boolean mark; - - public FileBlobStore(String dir) throws IOException { - baseDir = FilePath.get(dir); - FileUtils.createDirectories(dir); - } - - @Override - public String addBlob(String tempFilePath) { - try { - FilePath file = FilePath.get(tempFilePath); - InputStream in = file.newInputStream(); - MessageDigest messageDigest = MessageDigest.getInstance(HASH_ALGORITHM); - DigestInputStream din = new DigestInputStream(in, messageDigest); - long length = file.size(); - try { - while (true) { - int len = din.read(buffer, 0, buffer.length); - if (len < 0) { - break; - } - } - } finally { - din.close(); - } - ByteArrayOutputStream idStream = new ByteArrayOutputStream(); - idStream.write(TYPE_HASH); - IOUtils.writeVarInt(idStream, 0); - IOUtils.writeVarLong(idStream, length); - byte[] digest = messageDigest.digest(); - FilePath f = getFile(digest, false); - if (f.exists()) { - file.delete(); - } else { - FilePath parent = f.getParent(); - if (!parent.exists()) { - FileUtils.createDirectories(parent.toString()); - } - file.moveTo(f); - } - IOUtils.writeVarInt(idStream, digest.length); - idStream.write(digest); - byte[] id = idStream.toByteArray(); - String blobId = StringUtils.convertBytesToHex(id); - usesBlobId(blobId); - return blobId; - } catch (Exception e) { - throw ExceptionFactory.convert(e); - } - } - - @Override - protected synchronized void storeBlock(byte[] digest, int level, byte[] data) throws IOException { - FilePath f = getFile(digest, false); - if (f.exists()) { - return; - } - FilePath parent = f.getParent(); - if (!parent.exists()) { - FileUtils.createDirectories(parent.toString()); - } - FilePath temp = parent.resolve(f.getName() + ".temp"); - OutputStream out = temp.newOutputStream(false); - out.write(data); - out.close(); - temp.moveTo(f); - } - - private FilePath getFile(byte[] digest, boolean old) { - String id = StringUtils.convertBytesToHex(digest); - String sub = id.substring(id.length() - 2); - if (old) { - sub += OLD_SUFFIX; - } - return baseDir.resolve(sub).resolve(id + ".dat"); - } - - @Override - protected byte[] readBlockFromBackend(BlockId id) throws IOException { - FilePath f = getFile(id.digest, false); - if (!f.exists()) { - FilePath old = getFile(id.digest, true); - f.getParent().createDirectory(); - old.moveTo(f); - f = getFile(id.digest, false); - } - int length = (int) Math.min(f.size(), getBlockSize()); - byte[] data = new byte[length]; - InputStream in = f.newInputStream(); - try { - IOUtils.skipFully(in, id.pos); - IOUtils.readFully(in, data, 0, length); - } finally { - in.close(); - } - return data; - } - - @Override - public void startMark() throws Exception { - mark = true; - for (int i = 0; i < 256; i++) { - String sub = StringUtils.convertBytesToHex(new byte[] { (byte) i }); - FilePath d = baseDir.resolve(sub); - FilePath old = baseDir.resolve(sub + OLD_SUFFIX); - if (d.exists()) { - if (old.exists()) { - for (FilePath p : d.newDirectoryStream()) { - String name = p.getName(); - FilePath newName = old.resolve(name); - p.moveTo(newName); - } - } else { - d.moveTo(old); - } - } - } - markInUse(); - } - - @Override - protected boolean isMarkEnabled() { - return mark; - } - - @Override - protected void mark(BlockId id) throws IOException { - FilePath f = getFile(id.digest, false); - if (!f.exists()) { - FilePath old = getFile(id.digest, true); - f.getParent().createDirectory(); - old.moveTo(f); - f = getFile(id.digest, false); - } - } - - @Override - public int sweep() throws IOException { - int count = 0; - for (int i = 0; i < 256; i++) { - String sub = StringUtils.convertBytesToHex(new byte[] { (byte) i }); - FilePath old = baseDir.resolve(sub + OLD_SUFFIX); - if (old.exists()) { - for (FilePath p : old.newDirectoryStream()) { - String name = p.getName(); - FilePath file = old.resolve(name); - file.delete(); - count++; - } - old.delete(); - } - } - mark = false; - return count; - } - -} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/MongoBlobStore.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/MongoBlobStore.java deleted file mode 100644 index 255c1507f82..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/MongoBlobStore.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.blobs; - -import java.io.IOException; -import com.mongodb.BasicDBObject; -import com.mongodb.DB; -import com.mongodb.DBCollection; -import com.mongodb.DBObject; -import com.mongodb.Mongo; -import com.mongodb.MongoException; -import com.mongodb.WriteConcern; - -/** - * A blob store that uses MongoDB. - */ -public class MongoBlobStore extends AbstractBlobStore { - - private static final String DB = "ds"; - private static final String DATASTORE_COLLECTION = "dataStore"; - private static final String DIGEST_FIELD = "digest"; - private static final String DATA_FIELD = "data"; - - private Mongo con; - private DB db; - private DBCollection dataStore; - - public MongoBlobStore() throws IOException { - con = new Mongo(); - db = con.getDB(DB); - db.setWriteConcern(WriteConcern.SAFE); - dataStore = db.getCollection(DATASTORE_COLLECTION); - dataStore.ensureIndex( - new BasicDBObject(DIGEST_FIELD, 1), - new BasicDBObject("unique", true)); - } - - @Override - protected byte[] readBlockFromBackend(BlockId id) { - BasicDBObject key = new BasicDBObject(DIGEST_FIELD, id.digest); - DBObject dataObject = dataStore.findOne(key); - return (byte[]) dataObject.get(DATA_FIELD); - } - - @Override - protected void storeBlock(byte[] digest, int level, byte[] data) { - BasicDBObject dataObject = new BasicDBObject(DIGEST_FIELD, digest); - dataObject.append(DATA_FIELD, data); - try { - dataStore.insert(dataObject); - } catch (MongoException.DuplicateKey ignore) { - // ignore - } - } - - @Override - public void close() { - con.close(); - } - - @Override - public void startMark() throws Exception { - // TODO - markInUse(); - } - - @Override - protected boolean isMarkEnabled() { - // TODO - return false; - } - - @Override - protected void mark(BlockId id) throws Exception { - // TODO - } - - @Override - public int sweep() throws Exception { - // TODO - return 0; - } - -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/cluster/HotBackup.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/cluster/HotBackup.java deleted file mode 100644 index 7105192992a..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/cluster/HotBackup.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.cluster; - -import org.apache.jackrabbit.mk.api.MicroKernel; -import org.apache.jackrabbit.mk.json.JsopBuilder; -import org.apache.jackrabbit.mk.json.fast.Jsop; -import org.apache.jackrabbit.mk.json.fast.JsopArray; -import org.apache.jackrabbit.mk.json.fast.JsopObject; -import org.apache.jackrabbit.mk.util.PathUtils; - -/** - * This class connects two MicroKernel instances, where one - * instance will periodically commit the changes made to the other. - * - * TODO do a full sync on first call - * TODO add periodic background check - */ -public class HotBackup { - - private static final String PATH_PROPERTY_LASTREV = "/:lastrev"; - private final MicroKernel source; - private final MicroKernel target; - private String lastRev; - - /** - * Create a new instance of this class. - * - * @param source source microkernel where changes are read - * @param target target microkernel where changes are committed - */ - public HotBackup(MicroKernel source, MicroKernel target) { - this.source = source; - this.target = target; - - init(); - } - - private void init() { - lastRev = getProperty(target, PATH_PROPERTY_LASTREV); - if (lastRev == null) { - lastRev = source.getHeadRevision(); - - // TODO never sync'ed, so do a full copy - - - setProperty(target, PATH_PROPERTY_LASTREV, lastRev); - } - sync(); - } - - /** - * Read all changes from the source microkernel and commit them to - * the target microkernel. - */ - public void sync() { - String headRev = source.getHeadRevision(); - if (lastRev != headRev) { - JsopArray journal = (JsopArray) Jsop.parse(source.getJournal(lastRev, headRev, null)); - for (int i = 0; i < journal.size(); i++) { - JsopObject record = (JsopObject) journal.get(i); - String diff = (String) record.get("changes"); - String message = (String) record.get("msg"); - target.commit("", diff, target.getHeadRevision(), message); - } - lastRev = headRev; - setProperty(target, PATH_PROPERTY_LASTREV, lastRev); - } - } - - private static String getProperty(MicroKernel mk, String path) { - String parent = PathUtils.getParentPath(path); - String name = PathUtils.getName(path); - - // todo use filter parameter for specifying the property? - JsopObject props = (JsopObject) Jsop.parse(mk.getNodes(parent, mk.getHeadRevision(), -1, 0, -1, null)); - return (String) props.get(name); - } - - private static void setProperty(MicroKernel mk, String path, String value) { - String parent = PathUtils.getParentPath(path); - String name = PathUtils.getName(path); - - if (value == null) { - String diff = new JsopBuilder().tag('-').key(name).value(null).toString(); - mk.commit(parent, diff, mk.getHeadRevision(), null); - } else { - String diff = new JsopBuilder().tag('+').key(name).value(value).toString(); - mk.commit(parent, diff, mk.getHeadRevision(), null); - } - } -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FileBase.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FileBase.java deleted file mode 100644 index 7fd5be2ca98..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FileBase.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.fs; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; -import java.nio.channels.ReadableByteChannel; -import java.nio.channels.WritableByteChannel; - -/** - * The base class for file implementations. - */ -public abstract class FileBase extends FileChannel { - - public void force(boolean metaData) throws IOException { - // ignore - } - - public FileLock lock(long position, long size, boolean shared) throws IOException { - throw new UnsupportedOperationException(); - } - - public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException { - throw new UnsupportedOperationException(); - } - - public abstract long position() throws IOException; - - public abstract FileChannel position(long newPosition) throws IOException; - - public abstract int read(ByteBuffer dst) throws IOException; - - public int read(ByteBuffer dst, long position) throws IOException { - throw new UnsupportedOperationException(); - } - - public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { - throw new UnsupportedOperationException(); - } - - public abstract long size() throws IOException; - - public long transferFrom(ReadableByteChannel src, long position, long count) throws IOException { - throw new UnsupportedOperationException(); - } - - public long transferTo(long position, long count, WritableByteChannel target) - throws IOException { - throw new UnsupportedOperationException(); - } - - public abstract FileChannel truncate(long size) throws IOException; - - public FileLock tryLock(long position, long size, boolean shared) throws IOException { - throw new UnsupportedOperationException(); - } - - public abstract int write(ByteBuffer src) throws IOException; - - public int write(ByteBuffer src, long position) throws IOException { - throw new UnsupportedOperationException(); } - - public long write(ByteBuffer[] srcs, int offset, int length) throws IOException { - throw new UnsupportedOperationException(); } - - protected void implCloseChannel() throws IOException { - // ignore - } - -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FileCache.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FileCache.java deleted file mode 100644 index 40728ca0472..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FileCache.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.fs; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; -import org.apache.jackrabbit.mk.util.SimpleLRUCache; - -/** - * A file that has a simple read cache. - */ -public class FileCache extends FileBase { - - private static final boolean APPEND_BUFFER = !Boolean.getBoolean("mk.disableAppendBuffer"); - private static final int APPEND_BUFFER_SIZE_INIT = 8 * 1024; - private static final int APPEND_BUFFER_SIZE = 8 * 1024; - - private static final int BLOCK_SIZE = 4 * 1024; - private final String name; - private final Map readCache = SimpleLRUCache.newInstance(16); - private final FileChannel base; - private long pos, size; - - private AtomicReference appendBuffer; - private int appendOperations; - private Thread appendFlushThread; - - FileCache(String name, FileChannel base) throws IOException { - this.name = name; - this.base = base; - this.size = base.size(); - } - - public long position() throws IOException { - return pos; - } - - public FileChannel position(long newPosition) throws IOException { - this.pos = newPosition; - return this; - } - - boolean flush() throws IOException { - if (appendBuffer == null) { - return false; - } - synchronized (this) { - ByteArrayOutputStream newBuff = new ByteArrayOutputStream(APPEND_BUFFER_SIZE_INIT); - ByteArrayOutputStream buff = appendBuffer.getAndSet(newBuff); - if (buff.size() > 0) { - try { - base.position(size - buff.size()); - base.write(ByteBuffer.wrap(buff.toByteArray())); - } catch (IOException e) { - close(); - throw e; - } - } - } - return true; - } - - public int read(ByteBuffer dst) throws IOException { - flush(); - long readPos = (pos / BLOCK_SIZE) * BLOCK_SIZE; - int off = (int) (pos - readPos); - int len = BLOCK_SIZE - off; - ByteBuffer buff = readCache.get(readPos); - if (buff == null) { - base.position(readPos); - buff = ByteBuffer.allocate(BLOCK_SIZE); - int read = base.read(buff); - if (read == BLOCK_SIZE) { - readCache.put(readPos, buff); - } else { - if (read < 0) { - return -1; - } - len = Math.min(len, read); - } - } - len = Math.min(len, dst.remaining()); - System.arraycopy(buff.array(), off, dst.array(), dst.position(), len); - dst.position(dst.position() + len); - pos += len; - return len; - } - - public long size() throws IOException { - return size; - } - - public FileChannel truncate(long newSize) throws IOException { - flush(); - readCache.clear(); - base.truncate(newSize); - pos = Math.min(pos, newSize); - size = Math.min(size, newSize); - return this; - } - - public int write(ByteBuffer src) throws IOException { - if (readCache.size() > 0) { - readCache.clear(); - } - // append operations are buffered, but - // only if there was at least one successful write operation - // (to detect trying to write to a read-only file and such early on) - // (in addition to that, the first few append operations are not buffered - // to avoid starting a thread unnecessarily) - if (APPEND_BUFFER && pos == size && ++appendOperations >= 4) { - int len = src.remaining(); - if (len > APPEND_BUFFER_SIZE) { - flush(); - } else { - if (appendBuffer == null) { - ByteArrayOutputStream buff = new ByteArrayOutputStream(APPEND_BUFFER_SIZE_INIT); - appendBuffer = new AtomicReference(buff); - appendFlushThread = new Thread("Flush " + name) { - public void run() { - try { - do { - Thread.sleep(500); - if (flush()) { - continue; - } - } while (!Thread.interrupted()); - } catch (Exception e) { - // ignore - } - } - }; - appendFlushThread.setDaemon(true); - appendFlushThread.start(); - } - ByteArrayOutputStream buff = appendBuffer.get(); - if (buff.size() > APPEND_BUFFER_SIZE) { - flush(); - buff = appendBuffer.get(); - } - buff.write(src.array(), src.position(), len); - pos += len; - size += len; - return len; - } - } - base.position(pos); - int len = base.write(src); - pos += len; - size = Math.max(size, pos); - return len; - } - - protected void implCloseChannel() throws IOException { - if (appendBuffer != null) { - appendFlushThread.interrupt(); - try { - appendFlushThread.join(); - } catch (InterruptedException e) { - // ignore - } - flush(); - } - base.close(); - } - - public void force(boolean metaData) throws IOException { - flush(); - base.force(metaData); - } - - public FileLock tryLock(long position, long size, boolean shared) throws IOException { - flush(); - return base.tryLock(position, size, shared); - } - - public String toString() { - return "cache:" + base.toString(); - } - -} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FileChannelInputStream.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FileChannelInputStream.java deleted file mode 100644 index f41c856ad5b..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FileChannelInputStream.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License, - * Version 1.0, and under the Eclipse Public License, Version 1.0 - * (http://h2database.com/html/license.html). - * Initial Developer: H2 Group - */ -package org.apache.jackrabbit.mk.fs; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; - -/** - * Allows to read from a file channel like an input stream. - */ -public class FileChannelInputStream extends InputStream { - - private final FileChannel channel; - private final byte[] buffer = { 0 }; - private final boolean closeChannel; - - /** - * Create a new file object input stream from the file channel. - * - * @param channel the file channel - */ - public FileChannelInputStream(FileChannel channel, boolean closeChannel) { - this.channel = channel; - this.closeChannel = closeChannel; - } - - public int read() throws IOException { - if (channel.position() >= channel.size()) { - return -1; - } - FileUtils.readFully(channel, ByteBuffer.wrap(buffer)); - return buffer[0] & 0xff; - } - - public int read(byte[] b) throws IOException { - return read(b, 0, b.length); - } - - public int read(byte[] b, int off, int len) throws IOException { - if (channel.position() + len < channel.size()) { - FileUtils.readFully(channel, ByteBuffer.wrap(b, off, len)); - return len; - } - return super.read(b, off, len); - } - - public long skip(long n) throws IOException { - n = Math.min(channel.size() - channel.position(), n); - channel.position(channel.position() + n); - return n; - } - - public void close() throws IOException { - if (closeChannel) { - channel.close(); - } - } - -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FilePath.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FilePath.java deleted file mode 100644 index 216bf3fe678..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FilePath.java +++ /dev/null @@ -1,322 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.fs; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.channels.FileChannel; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; -import org.apache.jackrabbit.mk.util.StringUtils; - -/** - * A path to a file. It similar to the Java 7 java.nio.file.Path, - * but simpler, and works with older versions of Java. It also implements the - * relevant methods found in java.nio.file.FileSystem and - * FileSystems - */ -public abstract class FilePath { - - private static final FilePath DEFAULT = new FilePathDisk(); - - private static Map providers; - - /** - * The prefix for temporary files. - */ - private static String tempRandom; - private static long tempSequence; - - /** - * The complete path (which may be absolute or relative, depending on the - * file system). - */ - protected String name; - - /** - * Get the file path object for the given path. - * This method is similar to Java 7 java.nio.file.FileSystem.getPath. - * Windows-style '\' is replaced with '/'. - * - * @param path the path - * @return the file path object - */ - public static FilePath get(String path) { - path = path.replace('\\', '/'); - int index = path.indexOf(':'); - if (index < 2) { - // use the default provider if no prefix or - // only a single character (drive name) - return DEFAULT.getPath(path); - } - String scheme = path.substring(0, index); - registerDefaultProviders(); - FilePath p = providers.get(scheme); - if (p == null) { - // provider not found - use the default - p = DEFAULT; - } - return p.getPath(path); - } - - private static void registerDefaultProviders() { - if (providers == null) { - Map map = Collections.synchronizedMap(new HashMap()); - for (String c : new String[] { - "org.apache.jackrabbit.mk.fs.FilePathDisk", - "org.apache.jackrabbit.mk.fs.FilePathCache" - }) { - try { - FilePath p = (FilePath) Class.forName(c).newInstance(); - map.put(p.getScheme(), p); - } catch (Exception e) { - // ignore - the files may be excluded in purpose - } - } - providers = map; - } - } - - /** - * Register a file provider. - * - * @param provider the file provider - */ - public static void register(FilePath provider) { - registerDefaultProviders(); - providers.put(provider.getScheme(), provider); - } - - /** - * Unregister a file provider. - * - * @param provider the file provider - */ - public static void unregister(FilePath provider) { - registerDefaultProviders(); - providers.remove(provider.getScheme()); - } - - /** - * Get the size of a file in bytes - * - * @return the size in bytes - */ - public abstract long size(); - - /** - * Rename a file if this is allowed. - * - * @param newName the new fully qualified file name - */ - public abstract void moveTo(FilePath newName) throws IOException; - - /** - * Create a new file. - * - * @return true if creating was successful - */ - public abstract boolean createFile(); - - /** - * Checks if a file exists. - * - * @return true if it exists - */ - public abstract boolean exists(); - - /** - * Delete a file or directory if it exists. - * Directories may only be deleted if they are empty. - */ - public abstract void delete() throws IOException; - - /** - * List the files and directories in the given directory. - * - * @return the list of fully qualified file names - */ - public abstract List newDirectoryStream() throws IOException; - - /** - * Normalize a file name. - * - * @return the normalized file name - */ - public abstract FilePath toRealPath() throws IOException; - - /** - * Get the parent directory of a file or directory. - * - * @return the parent directory name - */ - public abstract FilePath getParent(); - - /** - * Check if it is a file or a directory. - * - * @return true if it is a directory - */ - public abstract boolean isDirectory(); - - /** - * Check if the file name includes a path. - * - * @return if the file name is absolute - */ - public abstract boolean isAbsolute(); - - /** - * Get the last modified date of a file - * - * @return the last modified date - */ - public abstract long lastModified(); - - /** - * Check if the file is writable. - * - * @return if the file is writable - */ - public abstract boolean canWrite(); - - /** - * Create a directory (all required parent directories already exist). - */ - public abstract void createDirectory() throws IOException; - - /** - * Get the file or directory name (the last element of the path). - * - * @return the last element of the path - */ - public String getName() { - int idx = Math.max(name.indexOf(':'), name.lastIndexOf('/')); - return idx < 0 ? name : name.substring(idx + 1); - } - - /** - * Create an output stream to write into the file. - * - * @param append if true, the file will grow, if false, the file will be - * truncated first - * @return the output stream - */ - public abstract OutputStream newOutputStream(boolean append) throws IOException; - - /** - * Open a random access file object. - * - * @param mode the access mode. Supported are r, rw, rws, rwd - * @return the file object - */ - public abstract FileChannel open(String mode) throws IOException; - - /** - * Create an input stream to read from the file. - * - * @return the input stream - */ - public abstract InputStream newInputStream() throws IOException; - - /** - * Disable the ability to write. - * - * @return true if the call was successful - */ - public abstract boolean setReadOnly(); - - /** - * Create a new temporary file. - * - * @param suffix the suffix - * @param deleteOnExit if the file should be deleted when the virtual - * machine exists - * @param inTempDir if the file should be stored in the temporary directory - * @return the name of the created file - */ - public FilePath createTempFile(String suffix, boolean deleteOnExit, boolean inTempDir) throws IOException { - while (true) { - FilePath p = getPath(name + getNextTempFileNamePart(false) + suffix); - if (p.exists() || !p.createFile()) { - // in theory, the random number could collide - getNextTempFileNamePart(true); - continue; - } - p.open("rw").close(); - return p; - } - } - - /** - * Get the next temporary file name part (the part in the middle). - * - * @param newRandom if the random part of the filename should change - * @return the file name part - */ - protected static synchronized String getNextTempFileNamePart(boolean newRandom) { - if (newRandom || tempRandom == null) { - byte[] prefix = new byte[8]; - new Random().nextBytes(prefix); - tempRandom = StringUtils.convertBytesToHex(prefix) + "."; - } - return tempRandom + tempSequence++; - } - - /** - * Get the string representation. The returned string can be used to - * construct a new object. - * - * @return the path as a string - */ - public String toString() { - return name; - } - - /** - * Get the scheme (prefix) for this file provider. - * This is similar to java.nio.file.spi.FileSystemProvider.getScheme. - * - * @return the scheme - */ - public abstract String getScheme(); - - /** - * Convert a file to a path. This is similar to - * java.nio.file.spi.FileSystemProvider.getPath, but may - * return an object even if the scheme doesn't match in case of the the - * default file provider. - * - * @param path the path - * @return the file path object - */ - public abstract FilePath getPath(String path); - - /** - * Append an element to the path. - * This is similar to java.nio.file.spi.FileSystemProvider.resolve. - * - * @param other the relative path (might be null) - * @return the resolved path - */ - public abstract FilePath resolve(String other); - -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FilePathDisk.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FilePathDisk.java deleted file mode 100644 index 2135ea01475..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FilePathDisk.java +++ /dev/null @@ -1,417 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.fs; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.RandomAccessFile; -import java.net.URL; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; -import java.util.ArrayList; -import java.util.List; - -/** - * This file system stores files on disk. - * This is the most common file system. - */ -public class FilePathDisk extends FilePath { - - private static final String CLASSPATH_PREFIX = "classpath:"; - private static final String FILE_SEPARATOR = System.getProperty("file.separator", "/"); - private static final int MAX_FILE_RETRY = 16; - - public FilePathDisk getPath(String path) { - FilePathDisk p = new FilePathDisk(); - p.name = translateFileName(path); - return p; - } - - public long size() { - return new File(name).length(); - } - - /** - * Translate the file name to the native format. This will replace '\' with - * '/' and expand the home directory ('~'). - * - * @param fileName the file name - * @return the native file name - */ - protected static String translateFileName(String fileName) { - fileName = fileName.replace('\\', '/'); - if (fileName.startsWith("file:")) { - fileName = fileName.substring("file:".length()); - } - return expandUserHomeDirectory(fileName); - } - - /** - * Expand '~' to the user home directory. It is only be expanded if the '~' - * stands alone, or is followed by '/' or '\'. - * - * @param fileName the file name - * @return the native file name - */ - public static String expandUserHomeDirectory(String fileName) { - if (fileName.startsWith("~") && (fileName.length() == 1 || fileName.startsWith("~/"))) { - String userDir = System.getProperty("user.home", ""); - fileName = userDir + fileName.substring(1); - } - return fileName; - } - - public void moveTo(FilePath newName) throws IOException { - File oldFile = new File(name); - File newFile = new File(newName.name); - if (oldFile.getAbsolutePath().equals(newFile.getAbsolutePath())) { - return; - } - if (!oldFile.exists()) { - throw new IOException("Could not rename " + - name + " (not found) to " + newName.name); - } - if (newFile.exists()) { - throw new IOException("Could not rename " + - name + " to " + newName + " (already exists)"); - } - for (int i = 0; i < MAX_FILE_RETRY; i++) { - boolean ok = oldFile.renameTo(newFile); - if (ok) { - return; - } - wait(i); - } - throw new IOException("Could not rename " + name + " to " + newName.name); - } - - private static void wait(int i) { - if (i == 8) { - System.gc(); - } - try { - // sleep at most 256 ms - long sleep = Math.min(256, i * i); - Thread.sleep(sleep); - } catch (InterruptedException e) { - // ignore - } - } - - public boolean createFile() { - File file = new File(name); - for (int i = 0; i < MAX_FILE_RETRY; i++) { - try { - return file.createNewFile(); - } catch (IOException e) { - // 'access denied' is really a concurrent access problem - wait(i); - } - } - return false; - } - - public boolean exists() { - return new File(name).exists(); - } - - public void delete() throws IOException { - File file = new File(name); - for (int i = 0; i < MAX_FILE_RETRY; i++) { - boolean ok = file.delete(); - if (ok || !file.exists()) { - return; - } - wait(i); - } - throw new IOException("Could not delete " + name); - } - - public List newDirectoryStream() throws IOException { - ArrayList list = new ArrayList(); - File f = new File(name); - String[] files = f.list(); - if (files != null) { - String base = f.getCanonicalPath(); - if (!base.endsWith(FILE_SEPARATOR)) { - base += FILE_SEPARATOR; - } - for (int i = 0, len = files.length; i < len; i++) { - list.add(getPath(base + files[i])); - } - } - return list; - } - - public boolean canWrite() { - return canWriteInternal(new File(name)); - } - - public boolean setReadOnly() { - File f = new File(name); - return f.setReadOnly(); - } - - public FilePathDisk toRealPath() throws IOException { - String fileName = new File(name).getCanonicalPath(); - return getPath(fileName); - } - - public FilePath getParent() { - String p = new File(name).getParent(); - return p == null ? null : getPath(p); - } - - public boolean isDirectory() { - return new File(name).isDirectory(); - } - - public boolean isAbsolute() { - return new File(name).isAbsolute(); - } - - public long lastModified() { - return new File(name).lastModified(); - } - - private static boolean canWriteInternal(File file) { - try { - if (!file.canWrite()) { - return false; - } - } catch (Exception e) { - // workaround for GAE which throws a - // java.security.AccessControlException - return false; - } - // File.canWrite() does not respect windows user permissions, - // so we must try to open it using the mode "rw". - // See also http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4420020 - RandomAccessFile r = null; - try { - r = new RandomAccessFile(file, "rw"); - return true; - } catch (FileNotFoundException e) { - return false; - } finally { - if (r != null) { - try { - r.close(); - } catch (IOException e) { - // ignore - } - } - } - } - - public void createDirectory() throws IOException { - File f = new File(name); - if (f.exists()) { - if (f.isDirectory()) { - return; - } - throw new IOException("A file with this name already exists: " + name); - } - File dir = new File(name); - for (int i = 0; i < MAX_FILE_RETRY; i++) { - if ((dir.exists() && dir.isDirectory()) || dir.mkdir()) { - return; - } - wait(i); - } - throw new IOException("Could not create " + name); - } - - public OutputStream newOutputStream(boolean append) throws IOException { - File file = new File(name); - File parent = file.getParentFile(); - if (parent != null) { - FileUtils.createDirectories(parent.getAbsolutePath()); - } - FileOutputStream out = new FileOutputStream(name, append); - return out; - } - - public InputStream newInputStream() throws IOException { - if (name.indexOf(':') > 1) { - // if the : is in position 1, a windows file access is assumed: C:.. or D: - if (name.startsWith(CLASSPATH_PREFIX)) { - String fileName = name.substring(CLASSPATH_PREFIX.length()); - if (!fileName.startsWith("/")) { - fileName = "/" + fileName; - } - InputStream in = getClass().getResourceAsStream(fileName); - if (in == null) { - Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName); - } - if (in == null) { - throw new FileNotFoundException("Resource " + fileName); - } - return in; - } - // otherwise an URL is assumed - URL url = new URL(name); - InputStream in = url.openStream(); - return in; - } - FileInputStream in = new FileInputStream(name); - return in; - } - - /** - * Call the garbage collection and run finalization. This close all files that - * were not closed, and are no longer referenced. - */ - static void freeMemoryAndFinalize() { - Runtime rt = Runtime.getRuntime(); - long mem = rt.freeMemory(); - for (int i = 0; i < 16; i++) { - rt.gc(); - long now = rt.freeMemory(); - rt.runFinalization(); - if (now == mem) { - break; - } - mem = now; - } - } - - public FileChannel open(String mode) throws IOException { - return new FileDisk(name, mode); - } - - public String getScheme() { - return "file"; - } - - public FilePath createTempFile(String suffix, boolean deleteOnExit, boolean inTempDir) - throws IOException { - String fileName = name + "."; - String prefix = new File(fileName).getName(); - File dir; - if (inTempDir) { - dir = new File(System.getProperty("java.io.tmpdir", ".")); - } else { - dir = new File(fileName).getAbsoluteFile().getParentFile(); - } - FileUtils.createDirectories(dir.getAbsolutePath()); - while (true) { - File f = new File(dir, prefix + getNextTempFileNamePart(false) + suffix); - if (f.exists() || !f.createNewFile()) { - // in theory, the random number could collide - getNextTempFileNamePart(true); - continue; - } - if (deleteOnExit) { - try { - f.deleteOnExit(); - } catch (Throwable e) { - // sometimes this throws a NullPointerException - // at java.io.DeleteOnExitHook.add(DeleteOnExitHook.java:33) - // we can ignore it - } - } - return get(f.getCanonicalPath()); - } - } - - public FilePath resolve(String other) { - return other == null ? this : getPath(name + "/" + other); - } - -} - -/** - * Uses java.io.RandomAccessFile to access a file. - */ -class FileDisk extends FileBase { - - private final RandomAccessFile file; - private final String name; - - private long pos; - - FileDisk(String fileName, String mode) throws FileNotFoundException { - this.file = new RandomAccessFile(fileName, mode); - this.name = fileName; - } - - public void force(boolean metaData) throws IOException { - file.getFD().sync(); - } - - public FileChannel truncate(long newLength) throws IOException { - if (newLength < file.length()) { - // some implementations actually only support truncate - file.setLength(newLength); - pos = Math.min(pos, newLength); - } - return this; - } - - public synchronized FileLock tryLock(long position, long size, boolean shared) throws IOException { - return file.getChannel().tryLock(); - } - - public void implCloseChannel() throws IOException { - file.close(); - } - - public long position() throws IOException { - return pos; - } - - public long size() throws IOException { - return file.length(); - } - - public int read(ByteBuffer dst) throws IOException { - int len = file.read(dst.array(), dst.position(), dst.remaining()); - if (len > 0) { - pos += len; - dst.position(dst.position() + len); - } - return len; - } - - public FileChannel position(long pos) throws IOException { - if (this.pos != pos) { - file.seek(pos); - this.pos = pos; - } - return this; - } - - public int write(ByteBuffer src) throws IOException { - int len = src.remaining(); - file.write(src.array(), src.position(), len); - src.position(src.position() + len); - pos += len; - return len; - } - - public String toString() { - return name; - } - -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FilePathWrapper.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FilePathWrapper.java deleted file mode 100644 index 8769082f18f..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FilePathWrapper.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License, - * Version 1.0, and under the Eclipse Public License, Version 1.0 - * (http://h2database.com/html/license.html). - * Initial Developer: H2 Group - */ -package org.apache.jackrabbit.mk.fs; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.channels.FileChannel; -import java.util.List; -import org.h2.message.DbException; - -/** - * The base class for wrapping / delegating file systems such as - * the split file system. - */ -public abstract class FilePathWrapper extends FilePath { - - private FilePath base; - - public FilePathWrapper getPath(String path) { - return create(path, unwrap(path)); - } - - /** - * Create a wrapped path instance for the given base path. - * - * @param base the base path - * @return the wrapped path - */ - public FilePathWrapper wrap(FilePath base) { - return base == null ? null : create(getPrefix() + base.name, base); - } - - public FilePath unwrap() { - return unwrap(name); - } - - private FilePathWrapper create(String path, FilePath base) { - try { - FilePathWrapper p = getClass().newInstance(); - p.name = path; - p.base = base; - return p; - } catch (Exception e) { - throw DbException.convert(e); - } - } - - protected String getPrefix() { - return getScheme() + ":"; - } - - /** - * Get the base path for the given wrapped path. - * - * @param path the path including the scheme prefix - * @return the base file path - */ - protected FilePath unwrap(String path) { - return FilePath.get(path.substring(getScheme().length() + 1)); - } - - protected FilePath getBase() { - return base; - } - - public boolean canWrite() { - return base.canWrite(); - } - - public void createDirectory() throws IOException { - base.createDirectory(); - } - - public boolean createFile() { - return base.createFile(); - } - - public void delete() throws IOException { - base.delete(); - } - - public boolean exists() { - return base.exists(); - } - - public FilePath getParent() { - return wrap(base.getParent()); - } - - public boolean isAbsolute() { - return base.isAbsolute(); - } - - public boolean isDirectory() { - return base.isDirectory(); - } - - public long lastModified() { - return base.lastModified(); - } - - public FilePath toRealPath() throws IOException { - return wrap(base.toRealPath()); - } - - public List newDirectoryStream() throws IOException { - List list = base.newDirectoryStream(); - for (int i = 0, len = list.size(); i < len; i++) { - list.set(i, wrap(list.get(i))); - } - return list; - } - - public void moveTo(FilePath newName) throws IOException { - base.moveTo(((FilePathWrapper) newName).base); - } - - public InputStream newInputStream() throws IOException { - return base.newInputStream(); - } - - public OutputStream newOutputStream(boolean append) throws IOException { - return base.newOutputStream(append); - } - - public FileChannel open(String mode) throws IOException { - return base.open(mode); - } - - public boolean setReadOnly() { - return base.setReadOnly(); - } - - public long size() { - return base.size(); - } - - public FilePath createTempFile(String suffix, boolean deleteOnExit, boolean inTempDir) - throws IOException { - return wrap(base.createTempFile(suffix, deleteOnExit, inTempDir)); - } - - public FilePath resolve(String other) { - return other == null ? this : wrap(base.resolve(other)); - } - -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FileUtils.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FileUtils.java deleted file mode 100644 index 8cb1beb41da..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FileUtils.java +++ /dev/null @@ -1,370 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.fs; - -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.util.ArrayList; -import java.util.List; -import org.apache.jackrabbit.mk.util.IOUtils; - -/** - * This utility class contains utility functions that use the file system - * abstraction. - */ -public class FileUtils { - - /** - * Checks if a file exists. - * This method is similar to Java 7 java.nio.file.Path.exists. - * - * @param fileName the file name - * @return true if it exists - */ - public static boolean exists(String fileName) { - return FilePath.get(fileName).exists(); - } - - /** - * Create a directory (all required parent directories must already exist). - * This method is similar to Java 7 java.nio.file.Path.createDirectory. - * - * @param directoryName the directory name - */ - public static void createDirectory(String directoryName) throws IOException { - FilePath.get(directoryName).createDirectory(); - } - - /** - * Create a new file. - * This method is similar to Java 7 java.nio.file.Path.createFile, but returns - * false instead of throwing a exception if the file already existed. - * - * @param fileName the file name - * @return true if creating was successful - */ - public static boolean createFile(String fileName) { - return FilePath.get(fileName).createFile(); - } - - /** - * Delete a file or directory if it exists. - * Directories may only be deleted if they are empty. - * This method is similar to Java 7 java.nio.file.Path.deleteIfExists. - * - * @param path the file or directory name - */ - public static void delete(String path) throws IOException { - FilePath.get(path).delete(); - } - - /** - * Get the canonical file or directory name. - * This method is similar to Java 7 java.nio.file.Path.toRealPath. - * - * @param fileName the file name - * @return the normalized file name - */ - public static String toRealPath(String fileName) throws IOException { - return FilePath.get(fileName).toRealPath().toString(); - } - - /** - * Get the parent directory of a file or directory. - * This method returns null if there is no parent. - * This method is similar to Java 7 java.nio.file.Path.getParent. - * - * @param fileName the file or directory name - * @return the parent directory name - */ - public static String getParent(String fileName) { - FilePath p = FilePath.get(fileName).getParent(); - return p == null ? null : p.toString(); - } - - /** - * Check if the file name includes a path. - * This method is similar to Java 7 java.nio.file.Path.isAbsolute. - * - * @param fileName the file name - * @return if the file name is absolute - */ - public static boolean isAbsolute(String fileName) { - return FilePath.get(fileName).isAbsolute(); - } - - /** - * Rename a file if this is allowed. - * This method is similar to Java 7 java.nio.file.Path.moveTo. - * - * @param oldName the old fully qualified file name - * @param newName the new fully qualified file name - */ - public static void moveTo(String oldName, String newName) throws IOException { - FilePath.get(oldName).moveTo(FilePath.get(newName)); - } - - /** - * Get the file or directory name (the last element of the path). - * This method is similar to Java 7 java.nio.file.Path.getName. - * - * @param path the directory and file name - * @return just the file name - */ - public static String getName(String path) { - return FilePath.get(path).getName(); - } - - /** - * List the files and directories in the given directory. - * This method is similar to Java 7 java.nio.file.Path.newDirectoryStream. - * - * @param path the directory - * @return the list of fully qualified file names - */ - public static List newDirectoryStream(String path) throws IOException { - List list = FilePath.get(path).newDirectoryStream(); - int len = list.size(); - List result = new ArrayList(len); - for (int i = 0; i < len; i++) { - result.add(list.get(i).toString()); - } - return result; - } - - /** - * Get the last modified date of a file. - * This method is similar to Java 7 - * java.nio.file.attribute.Attributes.readBasicFileAttributes(file).lastModified().toMillis() - * - * @param fileName the file name - * @return the last modified date - */ - public static long lastModified(String fileName) { - return FilePath.get(fileName).lastModified(); - } - - /** - * Get the size of a file in bytes - * This method is similar to Java 7 - * java.nio.file.attribute.Attributes.readBasicFileAttributes(file).size() - * - * @param fileName the file name - * @return the size in bytes - */ - public static long size(String fileName) { - return FilePath.get(fileName).size(); - } - - /** - * Check if it is a file or a directory. - * java.nio.file.attribute.Attributes.readBasicFileAttributes(file).isDirectory() - * - * @param fileName the file or directory name - * @return true if it is a directory - */ - public static boolean isDirectory(String fileName) { - return FilePath.get(fileName).isDirectory(); - } - - /** - * Open a random access file object. - * This method is similar to Java 7 java.nio.channels.FileChannel.open. - * - * @param fileName the file name - * @param mode the access mode. Supported are r, rw, rws, rwd - * @return the file object - */ - public static FileChannel open(String fileName, String mode) throws IOException { - return FilePath.get(fileName).open(mode); - } - - /** - * Create an input stream to read from the file. - * This method is similar to Java 7 java.nio.file.Path.newInputStream. - * - * @param fileName the file name - * @return the input stream - */ - public static InputStream newInputStream(String fileName) throws IOException { - return FilePath.get(fileName).newInputStream(); - } - - /** - * Create an output stream to write into the file. - * This method is similar to Java 7 java.nio.file.Path.newOutputStream. - * - * @param fileName the file name - * @param append if true, the file will grow, if false, the file will be - * truncated first - * @return the output stream - */ - public static OutputStream newOutputStream(String fileName, boolean append) throws IOException { - return FilePath.get(fileName).newOutputStream(append); - } - - /** - * Check if the file is writable. - * This method is similar to Java 7 - * java.nio.file.Path.checkAccess(AccessMode.WRITE) - * - * @param fileName the file name - * @return if the file is writable - */ - public static boolean canWrite(String fileName) { - return FilePath.get(fileName).canWrite(); - } - - // special methods ======================================= - - /** - * Disable the ability to write. The file can still be deleted afterwards. - * - * @param fileName the file name - * @return true if the call was successful - */ - public static boolean setReadOnly(String fileName) { - return FilePath.get(fileName).setReadOnly(); - } - - // utility methods ======================================= - - /** - * Delete a directory or file and all subdirectories and files. - * - * @param path the path - * @param tryOnly whether errors should be ignored - */ - public static void deleteRecursive(String path, boolean tryOnly) throws IOException { - if (exists(path)) { - if (isDirectory(path)) { - for (String s : newDirectoryStream(path)) { - deleteRecursive(s, tryOnly); - } - } - if (tryOnly) { - tryDelete(path); - } else { - delete(path); - } - } - } - - /** - * Create the directory and all required parent directories. - * - * @param dir the directory name - */ - public static void createDirectories(String dir) throws IOException { - if (dir != null) { - if (exists(dir)) { - if (!isDirectory(dir)) { - throw new IOException("Could not create directory, " + - "because a file with the same name already exists: " + dir); - } - } else { - String parent = getParent(dir); - createDirectories(parent); - createDirectory(dir); - } - } - } - - /** - * Copy a file from one directory to another, or to another file. - * - * @param original the original file name - * @param copy the file name of the copy - */ - public static void copy(String original, String copy) throws IOException { - InputStream in = newInputStream(original); - try { - OutputStream out = newOutputStream(copy, false); - try { - IOUtils.copy(in, out); - } finally { - out.close(); - } - } finally { - in.close(); - } - } - - /** - * Try to delete a file (ignore errors). - * - * @param fileName the file name - * @return true if it worked - */ - public static boolean tryDelete(String fileName) { - try { - FilePath.get(fileName).delete(); - return true; - } catch (Exception e) { - return false; - } - } - - /** - * Create a new temporary file. - * - * @param prefix the prefix of the file name (including directory name if - * required) - * @param suffix the suffix - * @param deleteOnExit if the file should be deleted when the virtual - * machine exists - * @param inTempDir if the file should be stored in the temporary directory - * @return the name of the created file - */ - public static String createTempFile(String prefix, String suffix, boolean deleteOnExit, boolean inTempDir) - throws IOException { - return FilePath.get(prefix).createTempFile(suffix, deleteOnExit, inTempDir).toString(); - } - - /** - * Fully read from the file. This will read all remaining bytes, - * or throw an EOFException if not successful. - * - * @param channel the file channel - * @param dst the byte buffer - */ - public static void readFully(FileChannel channel, ByteBuffer dst) throws IOException { - do { - int r = channel.read(dst); - if (r < 0) { - throw new EOFException(); - } - } while (dst.remaining() > 0); - } - - /** - * Fully write to the file. This will write all remaining bytes. - * - * @param channel the file channel - * @param src the byte buffer - */ - public static void writeFully(FileChannel channel, ByteBuffer src) throws IOException { - do { - channel.write(src); - } while (src.remaining() > 0); - } - -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/json/JsonBuilder.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/json/JsonBuilder.java deleted file mode 100644 index 8fdbe35d1f0..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/json/JsonBuilder.java +++ /dev/null @@ -1,445 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.jackrabbit.mk.json; - -import java.io.IOException; - -/** - * Partially based on json-simple - * Limitation: arrays can only have primitive members (i.e. no arrays nor objects) - */ -public final class JsonBuilder { - final Appendable writer; - - private JsonBuilder(Appendable writer) { - this.writer = writer; - } - - public static JsonObjectBuilder create(Appendable writer) throws IOException { - return new JsonBuilder(writer).new JsonObjectBuilder(null); - } - - public final class JsonObjectBuilder { - private final JsonObjectBuilder parent; - - private boolean hasKeys; - - public JsonObjectBuilder(JsonObjectBuilder parent) throws IOException { - this.parent = parent; - writer.append('{'); - } - - public JsonObjectBuilder value(String key, String value) throws IOException { - write(key, encode(value)); - return this; - } - - public JsonObjectBuilder valueEncoded(String key, String value) throws IOException { - write(key, value); - return this; - } - - public JsonObjectBuilder value(String key, int value) throws IOException { - write(key, encode(value)); - return this; - } - - public JsonObjectBuilder value(String key, long value) throws IOException { - write(key, encode(value)); - return this; - } - - public JsonObjectBuilder value(String key, float value) throws IOException { - write(key, encode(value)); - return this; - } - - public JsonObjectBuilder value(String key, double value) throws IOException { - write(key, encode(value)); - return this; - } - - public JsonObjectBuilder value(String key, Number value) throws IOException { - write(key, encode(value)); - return this; - } - - public JsonObjectBuilder value(String key, boolean value) throws IOException { - write(key, encode(value)); - return this; - } - - public JsonObjectBuilder nil(String key) throws IOException { - write(key, "null"); - return this; - } - - public JsonObjectBuilder array(String key, String[] value) throws IOException { - write(key, encode(value)); - return this; - } - - public JsonObjectBuilder array(String key, int[] value) throws IOException { - write(key, encode(value)); - return this; - } - - public JsonObjectBuilder array(String key, long[] value) throws IOException { - write(key, encode(value)); - return this; - } - - public JsonObjectBuilder array(String key, float[] value) throws IOException { - write(key, encode(value)); - return this; - } - - public JsonObjectBuilder array(String key, double[] value) throws IOException { - write(key, encode(value)); - return this; - } - - public JsonObjectBuilder array(String key, Number[] value) throws IOException { - write(key, encode(value)); - return this; - } - - public JsonObjectBuilder array(String key, boolean[] value) throws IOException { - write(key, encode(value)); - return this; - } - - public JsonArrayBuilder array(String key) throws IOException { - writeKey(key); - return new JsonArrayBuilder(this); - } - - public JsonObjectBuilder object(String key) throws IOException { - writeKey(key); - return new JsonObjectBuilder(this); - } - - public JsonObjectBuilder build() throws IOException { - writer.append('}'); - return parent; - } - - //------------------------------------------< private >--- - - private void optionalComma() throws IOException { - if (hasKeys) { - writer.append(','); - } else { - hasKeys = true; - } - } - - private void writeKey(String key) throws IOException { - optionalComma(); - writer.append(quote(escape(key))); - writer.append(':'); - } - - private void write(String key, String value) throws IOException { - writeKey(key); - writer.append(value); - } - - } - - public final class JsonArrayBuilder { - private final JsonObjectBuilder parent; - - private boolean hasValues; - - public JsonArrayBuilder(JsonObjectBuilder parent) throws IOException { - writer.append('['); - this.parent = parent; - } - - public JsonArrayBuilder value(String value) throws IOException { - optionalComma(); - writer.append(encode(value)); - return this; - } - - public JsonArrayBuilder value(int value) throws IOException { - optionalComma(); - writer.append(encode(value)); - return this; - } - - public JsonArrayBuilder value(long value) throws IOException { - optionalComma(); - writer.append(encode(value)); - return this; - } - - public JsonArrayBuilder value(float value) throws IOException { - optionalComma(); - writer.append(encode(value)); - return this; - } - - public JsonArrayBuilder value(double value) throws IOException { - optionalComma(); - writer.append(encode(value)); - return this; - } - - public JsonArrayBuilder value(Number value) throws IOException { - optionalComma(); - writer.append(encode(value)); - return this; - } - - public JsonArrayBuilder value(boolean value) throws IOException { - optionalComma(); - writer.append(encode(value)); - return this; - } - - public JsonArrayBuilder nil() throws IOException { - optionalComma(); - writer.append("null"); - return this; - } - - public JsonObjectBuilder build() throws IOException { - writer.append(']'); - return parent; - } - - //------------------------------------------< private >--- - - private void optionalComma() throws IOException { - if (hasValues) { - writer.append(','); - } else { - hasValues = true; - } - } - } - - /** - * Escape quotes, \, /, \r, \n, \b, \f, \t and other control characters (U+0000 through U+001F). - */ - public static String escape(String string) { - if (string == null) { - return null; - } - - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < string.length(); i++) { - char ch = string.charAt(i); - switch (ch) { - case '"': - sb.append("\\\""); - break; - case '\\': - sb.append("\\\\"); - break; - case '\b': - sb.append("\\b"); - break; - case '\f': - sb.append("\\f"); - break; - case '\n': - sb.append("\\n"); - break; - case '\r': - sb.append("\\r"); - break; - case '\t': - sb.append("\\t"); - break; - default: - //Reference: http://www.unicode.org/versions/Unicode5.1.0/ - if (ch >= '\u0000' && ch <= '\u001F' || - ch >= '\u007F' && ch <= '\u009F' || - ch >= '\u2000' && ch <= '\u20FF') { - - String ss = Integer.toHexString(ch); - sb.append("\\u"); - for (int k = 0; k < 4 - ss.length(); k++) { - sb.append('0'); - } - sb.append(ss.toUpperCase()); - } else { - sb.append(ch); - } - } - } - - return sb.toString(); - } - - public static String quote(String string) { - return '"' + string + '"'; - } - - public static String encode(String value) { - return quote(escape(value)); - } - - public static String encode(int value) { - return Integer.toString(value); - } - - public static String encode(long value) { - return Long.toString(value); - } - - public static String encode(float value) { - // TODO silently losing data, should probably throw an exception instead - return Float.isInfinite(value) || Float.isNaN(value) - ? "null" - : Float.toString(value); - } - - public static String encode(double value) { - // TODO silently losing data, should probably throw an exception instead - return Double.isInfinite(value) || Double.isNaN(value) - ? "null" - : Double.toString(value); - } - - public static String encode(Number value) { - return value.toString(); - } - - public static String encode(boolean value) { - return Boolean.toString(value); - } - - public static String encode(String[] values) { - if (values.length == 0) { - return "[]"; - } - - StringBuilder sb = new StringBuilder(); - sb.append('['); - for (String value : values) { - sb.append(encode(value)); - sb.append(','); - } - sb.deleteCharAt(sb.length() - 1); - sb.append(']'); - return sb.toString(); - } - - public static String encode(int[] values) { - if (values.length == 0) { - return "[]"; - } - - StringBuilder sb = new StringBuilder(); - sb.append('['); - for (int value : values) { - sb.append(encode(value)); - sb.append(','); - } - sb.deleteCharAt(sb.length() - 1); - sb.append(']'); - return sb.toString(); - } - - public static String encode(long[] values) { - if (values.length == 0) { - return "[]"; - } - - StringBuilder sb = new StringBuilder(); - sb.append('['); - for (long value : values) { - sb.append(encode(value)); - sb.append(','); - } - sb.deleteCharAt(sb.length() - 1); - sb.append(']'); - return sb.toString(); - } - - public static String encode(float[] values) { - if (values.length == 0) { - return "[]"; - } - - StringBuilder sb = new StringBuilder(); - sb.append('['); - for (float value : values) { - sb.append(encode(value)); - sb.append(','); - } - sb.deleteCharAt(sb.length() - 1); - sb.append(']'); - return sb.toString(); - } - - public static String encode(double[] values) { - if (values.length == 0) { - return "[]"; - } - - StringBuilder sb = new StringBuilder(); - sb.append('['); - for (double value : values) { - sb.append(encode(value)); - sb.append(','); - } - sb.deleteCharAt(sb.length() - 1); - sb.append(']'); - return sb.toString(); - } - - public static String encode(Number[] values) { - if (values.length == 0) { - return "[]"; - } - - StringBuilder sb = new StringBuilder(); - sb.append('['); - for (Number value : values) { - sb.append(encode(value)); - sb.append(','); - } - sb.deleteCharAt(sb.length() - 1); - sb.append(']'); - return sb.toString(); - } - - public static String encode(boolean[] values) { - if (values.length == 0) { - return "[]"; - } - - StringBuilder sb = new StringBuilder(); - sb.append('['); - for (boolean value : values) { - sb.append(encode(value)); - sb.append(','); - } - sb.deleteCharAt(sb.length() - 1); - sb.append(']'); - return sb.toString(); - } - -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/CommitBuilder.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/model/CommitBuilder.java deleted file mode 100644 index a109d5676b4..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/CommitBuilder.java +++ /dev/null @@ -1,494 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.model; - -import org.apache.jackrabbit.mk.store.NotFoundException; -import org.apache.jackrabbit.mk.store.RevisionStore; -import org.apache.jackrabbit.mk.util.PathUtils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -/** - * - */ -public class CommitBuilder { - - private Id baseRevId; - - private final String msg; - - private final RevisionStore store; - - // key is a path - private final Map staged = new HashMap(); - // change log - private final List changeLog = new ArrayList(); - - public CommitBuilder(Id baseRevId, String msg, RevisionStore store) throws Exception { - this.baseRevId = baseRevId; - this.msg = msg; - this.store = store; - } - - public void addNode(String parentNodePath, String nodeName) throws Exception { - addNode(parentNodePath, nodeName, Collections.emptyMap()); - } - - public void addNode(String parentNodePath, String nodeName, Map properties) throws Exception { - MutableNode modParent = getOrCreateStagedNode(parentNodePath); - if (modParent.getChildNodeEntry(nodeName) != null) { - throw new Exception("there's already a child node with name '" + nodeName + "'"); - } - MutableNode newChild = new MutableNode(store); - newChild.getProperties().putAll(properties); - - // id will be computed on commit - modParent.add(new ChildNodeEntry(nodeName, null)); - String newPath = PathUtils.concat(parentNodePath, nodeName); - staged.put(newPath, newChild); - // update change log - changeLog.add(new AddNode(parentNodePath, nodeName, properties)); - } - - public void removeNode(String nodePath) throws NotFoundException, Exception { - String parentPath = PathUtils.getParentPath(nodePath); - String nodeName = PathUtils.getName(nodePath); - - MutableNode parent = getOrCreateStagedNode(parentPath); - if (parent.remove(nodeName) == null) { - throw new NotFoundException(nodePath); - } - - // update staging area - removeStagedNodes(nodePath); - - // update change log - changeLog.add(new RemoveNode(nodePath)); - } - - public void moveNode(String srcPath, String destPath) throws NotFoundException, Exception { - if (PathUtils.isAncestor(srcPath, destPath)) { - throw new Exception("target path cannot be descendant of source path: " + destPath); - } - - String srcParentPath = PathUtils.getParentPath(srcPath); - String srcNodeName = PathUtils.getName(srcPath); - - String destParentPath = PathUtils.getParentPath(destPath); - String destNodeName = PathUtils.getName(destPath); - - MutableNode srcParent = getOrCreateStagedNode(srcParentPath); - if (srcParentPath.equals(destParentPath)) { - if (srcParent.getChildNodeEntry(destNodeName) != null) { - throw new Exception("node already exists at move destination path: " + destPath); - } - if (srcParent.rename(srcNodeName, destNodeName) == null) { - throw new NotFoundException(srcPath); - } - } else { - ChildNodeEntry srcCNE = srcParent.remove(srcNodeName); - if (srcCNE == null) { - throw new NotFoundException(srcPath); - } - - MutableNode destParent = getOrCreateStagedNode(destParentPath); - if (destParent.getChildNodeEntry(destNodeName) != null) { - throw new Exception("node already exists at move destination path: " + destPath); - } - destParent.add(new ChildNodeEntry(destNodeName, srcCNE.getId())); - } - - // update staging area - moveStagedNodes(srcPath, destPath); - - // update change log - changeLog.add(new MoveNode(srcPath, destPath)); - } - - public void copyNode(String srcPath, String destPath) throws NotFoundException, Exception { - String srcParentPath = PathUtils.getParentPath(srcPath); - String srcNodeName = PathUtils.getName(srcPath); - - String destParentPath = PathUtils.getParentPath(destPath); - String destNodeName = PathUtils.getName(destPath); - - MutableNode srcParent = getOrCreateStagedNode(srcParentPath); - ChildNodeEntry srcCNE = srcParent.getChildNodeEntry(srcNodeName); - if (srcCNE == null) { - throw new NotFoundException(srcPath); - } - - MutableNode destParent = getOrCreateStagedNode(destParentPath); - destParent.add(new ChildNodeEntry(destNodeName, srcCNE.getId())); - - // update change log - changeLog.add(new CopyNode(srcPath, destPath)); - } - - public void setProperty(String nodePath, String propName, String propValue) throws Exception { - MutableNode node = getOrCreateStagedNode(nodePath); - - Map properties = node.getProperties(); - if (propValue == null) { - properties.remove(propName); - } else { - properties.put(propName, propValue); - } - - // update change log - changeLog.add(new SetProperty(nodePath, propName, propValue)); - } - - public void setProperties(String nodePath, Map properties) throws Exception { - MutableNode node = getOrCreateStagedNode(nodePath); - - node.getProperties().clear(); - node.getProperties().putAll(properties); - - // update change log - changeLog.add(new SetProperties(nodePath, properties)); - } - - public Id /* new revId */ doCommit() throws Exception { - if (staged.isEmpty()) { - // nothing to commit - return baseRevId; - } - - Id currentHead = store.getHeadCommitId(); - if (!currentHead.equals(baseRevId)) { - // todo gracefully handle certain conflicts (e.g. changes on moved sub-trees, competing deletes etc) - // update base revision to new head - baseRevId = currentHead; - // clear staging area - staged.clear(); - // replay change log on new base revision - // copy log in order to avoid concurrent modifications - List log = new ArrayList(changeLog); - for (Change change : log) { - change.apply(); - } - } - - Id rootNodeId = persistStagedNodes(); - - Id newRevId; - store.lockHead(); - try { - currentHead = store.getHeadCommitId(); - if (!currentHead.equals(baseRevId)) { - StoredNode baseRoot = store.getRootNode(baseRevId); - StoredNode theirRoot = store.getRootNode(currentHead); - StoredNode ourRoot = store.getNode(rootNodeId); - - rootNodeId = mergeTree(baseRoot, ourRoot, theirRoot); - - baseRevId = currentHead; - } - - if (store.getCommit(currentHead).getRootNodeId().equals(rootNodeId)) { - // the commit didn't cause any changes, - // no need to create new commit object/update head revision - return currentHead; - } - MutableCommit newCommit = new MutableCommit(); - newCommit.setParentId(baseRevId); - newCommit.setCommitTS(System.currentTimeMillis()); - newCommit.setMsg(msg); - newCommit.setRootNodeId(rootNodeId); - newRevId = store.putCommit(newCommit); - - store.setHeadCommitId(newRevId); - } finally { - store.unlockHead(); - } - - // reset instance in order to be reusable - staged.clear(); - changeLog.clear(); - - return newRevId; - } - - MutableNode getOrCreateStagedNode(String nodePath) throws Exception { - MutableNode node = staged.get(nodePath); - if (node == null) { - MutableNode parent = staged.get("/"); - if (parent == null) { - parent = new MutableNode(store.getRootNode(baseRevId), store); - staged.put("/", parent); - } - node = parent; - String names[] = PathUtils.split(nodePath); - for (int i = names.length - 1; i >= 0; i--) { - String path = PathUtils.getAncestorPath(nodePath, i); - node = staged.get(path); - if (node == null) { - // not yet staged, resolve id using staged parent - // to allow for staged move operations - ChildNodeEntry cne = parent.getChildNodeEntry(names[names.length - i - 1]); - if (cne == null) { - throw new NotFoundException(nodePath); - } - node = new MutableNode(store.getNode(cne.getId()), store); - staged.put(path, node); - } - parent = node; - } - } - return node; - } - - void moveStagedNodes(String srcPath, String destPath) throws Exception { - MutableNode node = staged.get(srcPath); - if (node != null) { - staged.remove(srcPath); - staged.put(destPath, node); - for (Iterator it = node.getChildNodeNames(0, -1); it.hasNext(); ) { - String childName = it.next(); - moveStagedNodes(PathUtils.concat(srcPath, childName), PathUtils.concat(destPath, childName)); - } - } - } - - void removeStagedNodes(String nodePath) throws Exception { - MutableNode node = staged.get(nodePath); - if (node != null) { - staged.remove(nodePath); - for (Iterator it = node.getChildNodeNames(0, -1); it.hasNext(); ) { - String childName = it.next(); - removeStagedNodes(PathUtils.concat(nodePath, childName)); - } - } - } - - Id /* new id of root node */ persistStagedNodes() throws Exception { - // sort paths in in depth-descending order - ArrayList orderedPaths = new ArrayList(staged.keySet()); - Collections.sort(orderedPaths, new Comparator() { - public int compare(String path1, String path2) { - // paths should be ordered by depth, descending - int result = getDepth(path2) - getDepth(path1); - return (result != 0) ? result : 1; - } - - int getDepth(String path) { - return PathUtils.getDepth(path); - } - }); - // iterate over staged entries in depth-descending order - Id rootNodeId = null; - for (String path : orderedPaths) { - // persist node - Id id = store.putNode(staged.get(path)); - if (PathUtils.denotesRoot(path)) { - rootNodeId = id; - } else { - staged.get(PathUtils.getParentPath(path)).add(new ChildNodeEntry(PathUtils.getName(path), id)); - } - } - if (rootNodeId == null) { - throw new Exception("internal error: inconsistent staging area content"); - } - return rootNodeId; - } - - /** - * Performs a three-way merge of the trees rooted at ourRoot, - * theirRoot, using the tree at baseRoot as reference. - * - * @param baseRoot - * @param ourRoot - * @param theirRoot - * @return id of merged root node - * @throws Exception - */ - Id /* id of merged root node */ mergeTree(StoredNode baseRoot, StoredNode ourRoot, StoredNode theirRoot) throws Exception { - // as we're going to use the staging area for the merge process, - // we need to clear it first - staged.clear(); - - // recursively merge 'our' changes with 'their' changes... - mergeNode(baseRoot, ourRoot, theirRoot, "/"); - - return persistStagedNodes(); - } - - void mergeNode(StoredNode baseNode, StoredNode ourNode, StoredNode theirNode, String path) throws Exception { - NodeDelta theirChanges = new NodeDelta( - store, store.getNodeState(baseNode), store.getNodeState(theirNode)); - NodeDelta ourChanges = new NodeDelta( - store, store.getNodeState(baseNode), store.getNodeState(ourNode)); - - // merge non-conflicting changes - MutableNode mergedNode = new MutableNode(theirNode, store); - staged.put(path, mergedNode); - - mergedNode.getProperties().putAll(ourChanges.getAddedProperties()); - mergedNode.getProperties().putAll(ourChanges.getChangedProperties()); - for (String name : ourChanges.getRemovedProperties().keySet()) { - mergedNode.getProperties().remove(name); - } - - for (Map.Entry entry : ourChanges.getAddedChildNodes ().entrySet()) { - mergedNode.add(new ChildNodeEntry(entry.getKey(), entry.getValue())); - } - for (Map.Entry entry : ourChanges.getChangedChildNodes ().entrySet()) { - mergedNode.add(new ChildNodeEntry(entry.getKey(), entry.getValue())); - } - for (String name : ourChanges.getRemovedChildNodes().keySet()) { - mergedNode.remove(name); - } - - List conflicts = theirChanges.listConflicts(ourChanges); - // resolve/report merge conflicts - for (NodeDelta.Conflict conflict : conflicts) { - String conflictName = conflict.getName(); - String conflictPath = PathUtils.concat(path, conflictName); - switch (conflict.getType()) { - case PROPERTY_VALUE_CONFLICT: - throw new Exception( - "concurrent modification of property " + conflictPath - + " with conflicting values: \"" - + ourNode.getProperties().get(conflictName) - + "\", \"" - + theirNode.getProperties().get(conflictName)); - - case NODE_CONTENT_CONFLICT: { - if (ourChanges.getChangedChildNodes().containsKey(conflictName)) { - // modified subtrees - StoredNode baseChild = store.getNode(baseNode.getChildNodeEntry(conflictName).getId()); - StoredNode ourChild = store.getNode(ourNode.getChildNodeEntry(conflictName).getId()); - StoredNode theirChild = store.getNode(theirNode.getChildNodeEntry(conflictName).getId()); - // merge the dirty subtrees recursively - mergeNode(baseChild, ourChild, theirChild, PathUtils.concat(path, conflictName)); - } else { - // todo handle/merge colliding node creation - throw new Exception("colliding concurrent node creation: " + conflictPath); - } - break; - } - - case REMOVED_DIRTY_PROPERTY_CONFLICT: - mergedNode.getProperties().remove(conflictName); - break; - - case REMOVED_DIRTY_NODE_CONFLICT: - mergedNode.remove(conflictName); - break; - } - - } - } - - //--------------------------------------------------------< inner classes > - abstract class Change { - abstract void apply() throws Exception; - } - - class AddNode extends Change { - String parentNodePath; - String nodeName; - Map properties; - - AddNode(String parentNodePath, String nodeName, Map properties) { - this.parentNodePath = parentNodePath; - this.nodeName = nodeName; - this.properties = properties; - } - - void apply() throws Exception { - addNode(parentNodePath, nodeName, properties); - } - } - - class RemoveNode extends Change { - String nodePath; - - RemoveNode(String nodePath) { - this.nodePath = nodePath; - } - - void apply() throws Exception { - removeNode(nodePath); - } - } - - class MoveNode extends Change { - String srcPath; - String destPath; - - MoveNode(String srcPath, String destPath) { - this.srcPath = srcPath; - this.destPath = destPath; - } - - void apply() throws Exception { - moveNode(srcPath, destPath); - } - } - - class CopyNode extends Change { - String srcPath; - String destPath; - - CopyNode(String srcPath, String destPath) { - this.srcPath = srcPath; - this.destPath = destPath; - } - - void apply() throws Exception { - copyNode(srcPath, destPath); - } - } - - class SetProperty extends Change { - String nodePath; - String propName; - String propValue; - - SetProperty(String nodePath, String propName, String propValue) { - this.nodePath = nodePath; - this.propName = propName; - this.propValue = propValue; - } - - void apply() throws Exception { - setProperty(nodePath, propName, propValue); - } - } - - class SetProperties extends Change { - String nodePath; - Map properties; - - SetProperties(String nodePath, Map properties) { - this.nodePath = nodePath; - this.properties = properties; - } - - void apply() throws Exception { - setProperties(nodePath, properties); - } - } -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/NodeDiffHandler.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/model/NodeDiffHandler.java deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/NodeStateDiff.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/model/NodeStateDiff.java deleted file mode 100644 index 6f58c5f11fc..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/NodeStateDiff.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.model; - -import java.util.HashSet; -import java.util.Set; - -import org.apache.jackrabbit.oak.model.ChildNodeEntry; -import org.apache.jackrabbit.oak.model.NodeState; -import org.apache.jackrabbit.oak.model.PropertyState; - -/** - * Utility base class for comparing two {@link NodeState} instances. The - * {@link #compare(NodeState, NodeState)} method will go through all - * properties and child nodes of the two states, calling the relevant - * added, changed or deleted methods where appropriate. Differences in - * the ordering of properties or child nodes do not affect the comparison. - */ -public class NodeStateDiff { - - /** - * Called by {@link #compare(NodeState, NodeState)} for all added - * properties. The default implementation does nothing. - * - * @param after property state after the change - */ - public void propertyAdded(PropertyState after) { - } - - /** - * Called by {@link #compare(NodeState, NodeState)} for all changed - * properties. The names of the given two property states are guaranteed - * to be the same. The default implementation does nothing. - * - * @param before property state before the change - * @param after property state after the change - */ - public void propertyChanged(PropertyState before, PropertyState after) { - } - - /** - * Called by {@link #compare(NodeState, NodeState)} for all deleted - * properties. The default implementation does nothing. - * - * @param before property state before the change - */ - public void propertyDeleted(PropertyState before) { - } - - /** - * Called by {@link #compare(NodeState, NodeState)} for all added - * child nodes. The default implementation does nothing. - * - * @param name name of the added child node - * @param after child node state after the change - */ - public void childNodeAdded(String name, NodeState after) { - } - - /** - * Called by {@link #compare(NodeState, NodeState)} for all changed - * child nodes. The default implementation does nothing. - * - * @param name name of the changed child node - * @param before child node state before the change - * @param after child node state after the change - */ - public void childNodeChanged(String name, NodeState before, NodeState after) { - } - - /** - * Called by {@link #compare(NodeState, NodeState)} for all deleted - * child nodes. The default implementation does nothing. - * - * @param name name of the deleted child node - * @param before child node state before the change - */ - public void childNodeDeleted(String name, NodeState before) { - } - - /** - * Compares the given two node states. Any found differences are - * reported by calling the relevant added, changed or deleted methods. - * - * @param before node state before changes - * @param after node state after changes - */ - public void compare(NodeState before, NodeState after) { - compareProperties(before, after); - compareChildNodes(before, after); - } - - /** - * Compares the properties of the given two node states. - * - * @param before node state before changes - * @param after node state after changes - */ - private void compareProperties(NodeState before, NodeState after) { - Set beforeProperties = new HashSet(); - - for (PropertyState beforeProperty : before.getProperties()) { - String name = beforeProperty.getName(); - PropertyState afterProperty = after.getProperty(name); - if (afterProperty == null) { - propertyDeleted(beforeProperty); - } else { - beforeProperties.add(name); - if (!beforeProperty.equals(afterProperty)) { - propertyChanged(beforeProperty, afterProperty); - } - } - } - - for (PropertyState afterProperty : after.getProperties()) { - if (!beforeProperties.contains(afterProperty.getName())) { - propertyAdded(afterProperty); - } - } - } - - /** - * Compares the child nodes of the given two node states. - * - * @param before node state before changes - * @param after node state after changes - */ - private void compareChildNodes(NodeState before, NodeState after) { - Set beforeChildNodes = new HashSet(); - - for (ChildNodeEntry beforeCNE : before.getChildNodeEntries(0, -1)) { - String name = beforeCNE.getName(); - NodeState beforeChild = beforeCNE.getNode(); - NodeState afterChild = after.getChildNode(name); - if (afterChild == null) { - childNodeDeleted(name, beforeChild); - } else { - beforeChildNodes.add(name); - if (!beforeChild.equals(afterChild)) { - childNodeChanged(name, beforeChild, afterChild); - } - } - } - - for (ChildNodeEntry afterChild : after.getChildNodeEntries(0, -1)) { - String name = afterChild.getName(); - if (!beforeChildNodes.contains(name)) { - childNodeAdded(name, afterChild.getNode()); - } - } - } - -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/CopyingGC.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/store/CopyingGC.java deleted file mode 100644 index 7e451158901..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/CopyingGC.java +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.store; - -import java.io.Closeable; -import java.io.InputStream; -import java.util.Comparator; -import java.util.Iterator; -import java.util.TreeSet; - -import org.apache.jackrabbit.mk.model.ChildNodeEntriesMap; -import org.apache.jackrabbit.mk.model.ChildNodeEntry; -import org.apache.jackrabbit.mk.model.Id; -import org.apache.jackrabbit.mk.model.MutableCommit; -import org.apache.jackrabbit.mk.model.MutableNode; -import org.apache.jackrabbit.mk.model.StoredCommit; -import org.apache.jackrabbit.mk.model.StoredNode; -import org.apache.jackrabbit.mk.util.IOUtils; -import org.apache.jackrabbit.oak.model.NodeState; - -/** - * Revision garbage collector that copies reachable revisions from a "from" revision - * store to a "to" revision store. It assumes that both stores share the same blob - * store. - * - * In the current design, a revision is reachable, if it is either the head revision - * or requested during the GC cycle. - */ -public class CopyingGC implements RevisionStore, Closeable { - - /** - * From store. - */ - private RevisionStore rsFrom; - - /** - * To store. - */ - private RevisionStore rsTo; - - /** - * Flag indicating whether a GC cycle is running. - */ - private volatile boolean running; - - /** - * First commit id of "to" store. - */ - private Id firstCommitId; - - /** - * Map of commits that have been accessed while a GC cycle is running; these - * need to be "re-linked" with a preceding, possibly not adjacent parent - * commit before saving them back to the "to" revision store. - */ - private final TreeSet commits = new TreeSet( - new Comparator() { - public int compare(MutableCommit o1, MutableCommit o2) { - return o1.getId().compareTo(o2.getId()); - } - }); - - /** - * Create a new instance of this class. - * - * @param rsFrom from store - * @param rsTo to store - */ - public CopyingGC(RevisionStore rsFrom, RevisionStore rsTo) { - this.rsFrom = rsFrom; - this.rsTo = rsTo; - } - - /** - * Start GC cycle. - * - * @throws Exception if an error occurs - */ - public void start() throws Exception { - commits.clear(); - firstCommitId = rsTo.getHeadCommitId(); - - // Copy the head commit - MutableCommit commitTo = copy(rsFrom.getHeadCommit()); - commitTo.setParentId(rsTo.getHeadCommitId()); - Id revId = rsTo.putCommit(commitTo); - rsTo.setHeadCommitId(revId); - - // Add this as sentinel - commits.add(commitTo); - - running = true; - } - - /** - * Stop GC cycle. - */ - public void stop() throws Exception { - running = false; - - if (commits.size() > 1) { - Id parentId = firstCommitId; - for (MutableCommit commit : commits) { - commit.setParentId(parentId); - rsTo.putCommit(commit); - parentId = commit.getId(); - } - } - // TODO: swap rsFrom/rsTo and reset them - rsFrom = rsTo; - rsTo = null; - } - - public void close() { - if (rsFrom instanceof Closeable) { - IOUtils.closeQuietly((Closeable) rsFrom); - } - if (rsTo instanceof Closeable) { - IOUtils.closeQuietly((Closeable) rsTo); - } - } - - /** - * Copy a commit and all the nodes belonging to it, starting at the root node. - * - * @param commit commit to copy - * @return commit in the "to" store, not yet persisted - * @throws Exception if an error occurs - */ - private MutableCommit copy(StoredCommit commit) throws Exception { - StoredNode nodeFrom = rsFrom.getNode(commit.getRootNodeId()); - copy(nodeFrom); - - return new MutableCommit(commit); - } - - /** - * Copy a node and all its descendants into a target store - * @param node source node - * @throws Exception if an error occurs - */ - private void copy(StoredNode node) throws Exception { - try { - rsTo.getNode(node.getId()); - return; - } catch (NotFoundException e) { - // ignore, better add a has() method - } - rsTo.putNode(new MutableNode(node, rsTo)); - - Iterator iter = node.getChildNodeEntries(0, -1); - while (iter.hasNext()) { - ChildNodeEntry c = iter.next(); - copy(rsFrom.getNode(c.getId())); - } - } - - // ---------------------------------------------------------- RevisionStore - - public NodeState getNodeState(StoredNode node) { - return new StoredNodeAsState(node, this); - } - - public Id getId(NodeState node) { - return ((StoredNodeAsState) node).getId(); - } - - public StoredNode getNode(Id id) throws NotFoundException, Exception { - if (running) { - try { - return rsTo.getNode(id); - } catch (NotFoundException e) { - // ignore, better add a has() method - } - } - return rsFrom.getNode(id); - } - - public StoredCommit getCommit(Id id) throws NotFoundException, - Exception { - - if (running) { - try { - return rsTo.getCommit(id); - } catch (NotFoundException e) { - // ignore, better add a has() method - } - } - return rsFrom.getCommit(id); - } - - public ChildNodeEntriesMap getCNEMap(Id id) throws NotFoundException, - Exception { - - if (running) { - try { - return rsTo.getCNEMap(id); - } catch (NotFoundException e) { - // ignore, better add a has() method - } - } - return rsFrom.getCNEMap(id); - } - - public StoredNode getRootNode(Id commitId) throws NotFoundException, - Exception { - - if (running) { - try { - return rsTo.getRootNode(commitId); - } catch (NotFoundException e) { - // ignore, better add a has() method - } - } - // Copy this commit - StoredCommit commit = rsFrom.getCommit(commitId); - if (running) { - commits.add(copy(commit)); - } - return rsFrom.getNode(commit.getRootNodeId()); - } - - public StoredCommit getHeadCommit() throws Exception { - return running ? rsTo.getHeadCommit() : rsFrom.getHeadCommit(); - } - - public Id getHeadCommitId() throws Exception { - return running ? rsTo.getHeadCommitId() : rsFrom.getHeadCommitId(); - } - - public Id putNode(MutableNode node) throws Exception { - return running ? rsTo.putNode(node) : rsFrom.putNode(node); - } - - public Id putCommit(MutableCommit commit) throws Exception { - return running ? rsTo.putCommit(commit) : rsFrom.putCommit(commit); - } - - public Id putCNEMap(ChildNodeEntriesMap map) throws Exception { - return running ? rsTo.putCNEMap(map) : rsFrom.putCNEMap(map); - } - - // TODO: potentially dangerous, if lock & unlock interfere with GC start - public void lockHead() { - if (running) { - rsTo.lockHead(); - } else { - rsFrom.lockHead(); - } - } - - public void setHeadCommitId(Id commitId) throws Exception { - if (running) { - rsTo.setHeadCommitId(commitId); - } else { - rsFrom.setHeadCommitId(commitId); - } - } - - // TODO: potentially dangerous, if lock & unlock interfere with GC start - public void unlockHead() { - if (running) { - rsTo.unlockHead(); - } else { - rsFrom.unlockHead(); - } - } - - public int getBlob(String blobId, long pos, byte[] buff, int off, int length) - throws NotFoundException, Exception { - - // Assuming that from and to store use the same BlobStore instance - return rsTo.getBlob(blobId, pos, buff, off, length); - } - - public long getBlobLength(String blobId) throws NotFoundException, - Exception { - - // Assuming that from and to store use the same BlobStore instance - return rsTo.getBlobLength(blobId); - } - - public String putBlob(InputStream in) throws Exception { - // Assuming that from and to store use the same BlobStore instance - return rsTo.putBlob(in); - } -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/DefaultRevisionStore.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/store/DefaultRevisionStore.java deleted file mode 100644 index 57a7dceb854..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/DefaultRevisionStore.java +++ /dev/null @@ -1,320 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.store; - -import org.apache.jackrabbit.mk.blobs.BlobStore; -import org.apache.jackrabbit.mk.blobs.FileBlobStore; -import org.apache.jackrabbit.mk.model.ChildNodeEntriesMap; -import org.apache.jackrabbit.mk.model.Id; -import org.apache.jackrabbit.mk.model.MutableCommit; -import org.apache.jackrabbit.mk.model.MutableNode; -import org.apache.jackrabbit.mk.model.StoredCommit; -import org.apache.jackrabbit.mk.model.StoredNode; -import org.apache.jackrabbit.mk.store.persistence.H2Persistence; -import org.apache.jackrabbit.mk.store.persistence.Persistence; -import org.apache.jackrabbit.mk.util.SimpleLRUCache; -import org.apache.jackrabbit.oak.model.NodeState; - -import java.io.Closeable; -import java.io.File; -import java.io.InputStream; -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -/** - * Default revision store implementation, passing calls to a Persistence - * and a BlobStore, respectively and providing caching. - */ -public class DefaultRevisionStore implements RevisionStore, Closeable { - - public static final String CACHE_SIZE = "mk.cacheSize"; - public static final int DEFAULT_CACHE_SIZE = 10000; - - private boolean initialized; - - private Id head; - private long headCounter; - private final ReentrantReadWriteLock headLock = new ReentrantReadWriteLock(); - private Persistence pm; - private BlobStore blobStore; - private boolean blobStoreNeedsClose; - - private Map cache; - - public void initialize(File homeDir) throws Exception { - if (initialized) { - throw new IllegalStateException("already initialized"); - } - - cache = Collections.synchronizedMap(SimpleLRUCache.newInstance(determineInitialCacheSize())); - - pm = new H2Persistence(); - //pm = new InMemPersistence(); - //pm = new MongoPersistence(); - //pm = new BDbPersistence(); - //pm = new FSPersistence(); - pm.initialize(homeDir); - - if (pm instanceof BlobStore) { - blobStore = (BlobStore) pm; - } else { - blobStore = new FileBlobStore(new File(homeDir, "blobs").getCanonicalPath()); - blobStoreNeedsClose = true; - } - - // make sure we've got a HEAD commit - head = pm.readHead(); - if (head == null || head.getBytes().length == 0) { - // assume virgin repository - byte[] rawHead = longToBytes(++headCounter); - head = new Id(rawHead); - - Id rootNodeId = pm.writeNode(new MutableNode(this)); - MutableCommit initialCommit = new MutableCommit(); - initialCommit.setCommitTS(System.currentTimeMillis()); - initialCommit.setRootNodeId(rootNodeId); - pm.writeCommit(head, initialCommit); - pm.writeHead(head); - } else { - headCounter = Long.parseLong(head.toString(), 16); - } - - initialized = true; - } - - public void close() { - verifyInitialized(); - - cache.clear(); - - if (blobStoreNeedsClose) { - blobStore.close(); - } - pm.close(); - - initialized = false; - } - - protected void verifyInitialized() { - if (!initialized) { - throw new IllegalStateException("not initialized"); - } - } - - protected int determineInitialCacheSize() { - String val = System.getProperty(CACHE_SIZE); - return (val != null) ? Integer.parseInt(val) : DEFAULT_CACHE_SIZE; - } - - /** - * Convert a long value into a fixed-size byte array of size 8. - * - * @param value value - * @return byte array - */ - private static byte[] longToBytes(long value) { - byte[] result = new byte[8]; - - for (int i = result.length - 1; i >= 0 && value != 0; i--) { - result[i] = (byte) (value & 0xff); - value >>>= 8; - } - return result; - } - - //--------------------------------------------------------< RevisionStore > - - public Id putNode(MutableNode node) throws Exception { - verifyInitialized(); - - PersistHook callback = null; - if (node instanceof PersistHook) { - callback = (PersistHook) node; - callback.prePersist(this); - } - - Id id = pm.writeNode(node); - - if (callback != null) { - callback.postPersist(this); - } - - cache.put(id, new StoredNode(id, node, this)); - - return id; - } - - public Id putCNEMap(ChildNodeEntriesMap map) throws Exception { - verifyInitialized(); - - PersistHook callback = null; - if (map instanceof PersistHook) { - callback = (PersistHook) map; - callback.prePersist(this); - } - - Id id = pm.writeCNEMap(map); - - if (callback != null) { - callback.postPersist(this); - } - - cache.put(id, map); - - return id; - } - - public Id putCommit(MutableCommit commit) throws Exception { - verifyInitialized(); - - PersistHook callback = null; - if (commit instanceof PersistHook) { - callback = (PersistHook) commit; - callback.prePersist(this); - } - - Id id = commit.getId(); - if (id == null) { - id = new Id(longToBytes(++headCounter)); - } - pm.writeCommit(id, commit); - - if (callback != null) { - callback.postPersist(this); - } - cache.put(id, new StoredCommit(id, commit)); - return id; - } - - public void setHeadCommitId(Id id) throws Exception { - verifyInitialized(); - - headLock.writeLock().lock(); - try { - pm.writeHead(id); - head = id; - - long headCounter = Long.parseLong(id.toString(), 16); - if (headCounter > this.headCounter) { - this.headCounter = headCounter; - } - } finally { - headLock.writeLock().unlock(); - } - } - - public void lockHead() { - headLock.writeLock().lock(); - } - - public void unlockHead() { - headLock.writeLock().unlock(); - } - - public String putBlob(InputStream in) throws Exception { - verifyInitialized(); - - return blobStore.writeBlob(in); - } - - //-----------------------------------------------------< RevisionProvider > - - public NodeState getNodeState(StoredNode node) { - return new StoredNodeAsState(node, this); - } - - public Id getId(NodeState node) { - return ((StoredNodeAsState) node).getId(); - } - - public StoredNode getNode(Id id) throws NotFoundException, Exception { - verifyInitialized(); - - StoredNode node = (StoredNode) cache.get(id); - if (node != null) { - return node; - } - - Binding nodeBinding = pm.readNodeBinding(id); - node = StoredNode.deserialize(id, this, nodeBinding); - - cache.put(id, node); - - return node; - } - - public ChildNodeEntriesMap getCNEMap(Id id) throws NotFoundException, Exception { - verifyInitialized(); - - ChildNodeEntriesMap map = (ChildNodeEntriesMap) cache.get(id); - if (map != null) { - return map; - } - - map = pm.readCNEMap(id); - - cache.put(id, map); - - return map; - } - - public StoredCommit getCommit(Id id) throws NotFoundException, Exception { - verifyInitialized(); - - StoredCommit commit = (StoredCommit) cache.get(id); - if (commit != null) { - return commit; - } - - commit = pm.readCommit(id); - cache.put(id, commit); - - return commit; - } - - public StoredNode getRootNode(Id commitId) throws NotFoundException, Exception { - return getNode(getCommit(commitId).getRootNodeId()); - } - - public StoredCommit getHeadCommit() throws Exception { - return getCommit(getHeadCommitId()); - } - - public Id getHeadCommitId() throws Exception { - verifyInitialized(); - - headLock.readLock().lock(); - try { - return head; - } finally { - headLock.readLock().unlock(); - } - } - - public int getBlob(String blobId, long pos, byte[] buff, int off, int length) throws NotFoundException, Exception { - verifyInitialized(); - - return blobStore.readBlob(blobId, pos, buff, off, length); - } - - public long getBlobLength(String blobId) throws NotFoundException, Exception { - verifyInitialized(); - - return blobStore.getBlobLength(blobId); - } -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/persistence/BDbPersistence.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/store/persistence/BDbPersistence.java deleted file mode 100644 index 9ead947c0bf..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/persistence/BDbPersistence.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.store.persistence; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; - -import org.apache.jackrabbit.mk.model.ChildNodeEntriesMap; -import org.apache.jackrabbit.mk.model.Commit; -import org.apache.jackrabbit.mk.model.Id; -import org.apache.jackrabbit.mk.model.Node; -import org.apache.jackrabbit.mk.model.StoredCommit; -import org.apache.jackrabbit.mk.store.BinaryBinding; -import org.apache.jackrabbit.mk.store.Binding; -import org.apache.jackrabbit.mk.store.IdFactory; -import org.apache.jackrabbit.mk.store.NotFoundException; - -import com.sleepycat.je.Database; -import com.sleepycat.je.DatabaseConfig; -import com.sleepycat.je.DatabaseEntry; -import com.sleepycat.je.Durability; -import com.sleepycat.je.Environment; -import com.sleepycat.je.EnvironmentConfig; -import com.sleepycat.je.EnvironmentMutableConfig; -import com.sleepycat.je.LockMode; -import com.sleepycat.je.OperationStatus; - -/** - * - */ -public class BDbPersistence implements Persistence { - - private final static byte[] HEAD_ID = new byte[]{0}; - private Environment dbEnv; - private Database db; - private Database head; - - // TODO: make this configurable - private IdFactory idFactory = IdFactory.getDigestFactory(); - - public void initialize(File homeDir) throws Exception { - File dbDir = new File(homeDir, "db"); - if (!dbDir.exists()) { - dbDir.mkdirs(); - } - - EnvironmentConfig envConfig = new EnvironmentConfig(); - //envConfig.setTransactional(true); - envConfig.setAllowCreate(true); - dbEnv = new Environment(dbDir, envConfig); - - EnvironmentMutableConfig envMutableConfig = new EnvironmentMutableConfig(); - //envMutableConfig.setDurability(Durability.COMMIT_SYNC); - //envMutableConfig.setDurability(Durability.COMMIT_NO_SYNC); - envMutableConfig.setDurability(Durability.COMMIT_WRITE_NO_SYNC); - dbEnv.setMutableConfig(envMutableConfig); - - DatabaseConfig dbConfig = new DatabaseConfig(); - dbConfig.setAllowCreate(true); - //dbConfig.setDeferredWrite(true); - db = dbEnv.openDatabase(null, "revs", dbConfig); - - head = dbEnv.openDatabase(null, "head", dbConfig); - - // TODO FIXME workaround in case we're not closed properly - Runtime.getRuntime().addShutdownHook(new Thread() { - public void run() { - try { close(); } catch (Throwable ignore) {} - } - }); - } - - public void close() { - try { - if (db.getConfig().getDeferredWrite()) { - db.sync(); - } - db.close(); - head.close(); - dbEnv.close(); - - db = null; - dbEnv = null; - } catch (Throwable t) { - t.printStackTrace(); - } - } - - public Id readHead() throws Exception { - DatabaseEntry key = new DatabaseEntry(HEAD_ID); - DatabaseEntry data = new DatabaseEntry(); - - if (head.get(null, key, data, LockMode.DEFAULT) == OperationStatus.SUCCESS) { - return new Id(data.getData()); - } else { - return null; - } - } - - public void writeHead(Id id) throws Exception { - DatabaseEntry key = new DatabaseEntry(HEAD_ID); - DatabaseEntry data = new DatabaseEntry(id.getBytes()); - - head.put(null, key, data); - } - - public Binding readNodeBinding(Id id) throws NotFoundException, Exception { - DatabaseEntry key = new DatabaseEntry(id.getBytes()); - DatabaseEntry data = new DatabaseEntry(); - - if (db.get(null, key, data, LockMode.DEFAULT) == OperationStatus.SUCCESS) { - ByteArrayInputStream in = new ByteArrayInputStream(data.getData()); - return new BinaryBinding(in); - } else { - throw new NotFoundException(id.toString()); - } - } - - public Id writeNode(Node node) throws Exception { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - node.serialize(new BinaryBinding(out)); - byte[] bytes = out.toByteArray(); - Id id = new Id(idFactory.createContentId(bytes)); - persist(id.getBytes(), bytes); - return id; - } - - public StoredCommit readCommit(Id id) throws NotFoundException, Exception { - DatabaseEntry key = new DatabaseEntry(id.getBytes()); - DatabaseEntry data = new DatabaseEntry(); - - if (db.get(null, key, data, LockMode.DEFAULT) == OperationStatus.SUCCESS) { - ByteArrayInputStream in = new ByteArrayInputStream(data.getData()); - return StoredCommit.deserialize(id, new BinaryBinding(in)); - } else { - throw new NotFoundException(id.toString()); - } - } - - public void writeCommit(Id id, Commit commit) throws Exception { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - commit.serialize(new BinaryBinding(out)); - byte[] bytes = out.toByteArray(); - persist(id.getBytes(), bytes); - } - - public ChildNodeEntriesMap readCNEMap(Id id) throws NotFoundException, Exception { - DatabaseEntry key = new DatabaseEntry(id.getBytes()); - DatabaseEntry data = new DatabaseEntry(); - - if (db.get(null, key, data, LockMode.DEFAULT) == OperationStatus.SUCCESS) { - ByteArrayInputStream in = new ByteArrayInputStream(data.getData()); - return ChildNodeEntriesMap.deserialize(new BinaryBinding(in)); - } else { - throw new NotFoundException(id.toString()); - } - } - - public Id writeCNEMap(ChildNodeEntriesMap map) throws Exception { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - map.serialize(new BinaryBinding(out)); - byte[] bytes = out.toByteArray(); - Id id = new Id(idFactory.createContentId(bytes)); - persist(id.getBytes(), bytes); - return id; - } - - //-------------------------------------------------------< implementation > - - protected void persist(byte[] rawId, byte[] bytes) throws Exception { - DatabaseEntry key = new DatabaseEntry(rawId); - DatabaseEntry data = new DatabaseEntry(bytes); - - db.put(null, key, data); - } -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/persistence/FSPersistence.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/store/persistence/FSPersistence.java deleted file mode 100644 index dd0fb42a211..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/persistence/FSPersistence.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.store.persistence; - -import java.io.BufferedInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; - -import org.apache.jackrabbit.mk.model.ChildNodeEntriesMap; -import org.apache.jackrabbit.mk.model.Commit; -import org.apache.jackrabbit.mk.model.Id; -import org.apache.jackrabbit.mk.model.Node; -import org.apache.jackrabbit.mk.model.StoredCommit; -import org.apache.jackrabbit.mk.store.BinaryBinding; -import org.apache.jackrabbit.mk.store.Binding; -import org.apache.jackrabbit.mk.store.IdFactory; -import org.apache.jackrabbit.mk.store.NotFoundException; -import org.apache.jackrabbit.mk.util.IOUtils; - -/** - * - */ -public class FSPersistence implements Persistence { - - private File dataDir; - private File head; - - // TODO: make this configurable - private IdFactory idFactory = IdFactory.getDigestFactory(); - - public void initialize(File homeDir) throws Exception { - dataDir = new File(homeDir, "data"); - if (!dataDir.exists()) { - dataDir.mkdirs(); - } - head = new File(homeDir, "HEAD"); - if (!head.exists()) { - writeHead(null); - } - } - - public void close() { - } - - public Id readHead() throws Exception { - FileInputStream in = new FileInputStream(head); - try { - String s = IOUtils.readString(in); - return s.equals("") ? null : Id.fromString(s); - } finally { - in.close(); - } - } - - public void writeHead(Id id) throws Exception { - FileOutputStream out = new FileOutputStream(head); - try { - IOUtils.writeString(out, id == null ? "" : id.toString()); - } finally { - out.close(); - } - } - - public Binding readNodeBinding(Id id) throws NotFoundException, Exception { - File f = getFile(id); - if (f.exists()) { - BufferedInputStream in = new BufferedInputStream(new FileInputStream(f)); - try { - return new BinaryBinding(in); - } finally { - in.close(); - } - } else { - throw new NotFoundException(id.toString()); - } - } - - public Id writeNode(Node node) throws Exception { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - node.serialize(new BinaryBinding(out)); - byte[] bytes = out.toByteArray(); - Id id = new Id(idFactory.createContentId(bytes)); - writeFile(id, bytes); - return id; - } - - public StoredCommit readCommit(Id id) throws NotFoundException, Exception { - File f = getFile(id); - if (f.exists()) { - BufferedInputStream in = new BufferedInputStream(new FileInputStream(f)); - try { - return StoredCommit.deserialize(id, new BinaryBinding(in)); - } finally { - in.close(); - } - } else { - throw new NotFoundException(id.toString()); - } - } - - public void writeCommit(Id id, Commit commit) throws Exception { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - commit.serialize(new BinaryBinding(out)); - byte[] bytes = out.toByteArray(); - writeFile(id, bytes); - } - - public ChildNodeEntriesMap readCNEMap(Id id) throws NotFoundException, Exception { - File f = getFile(id); - if (f.exists()) { - BufferedInputStream in = new BufferedInputStream(new FileInputStream(f)); - try { - return ChildNodeEntriesMap.deserialize(new BinaryBinding(in)); - } finally { - in.close(); - } - } else { - throw new NotFoundException(id.toString()); - } - } - - public Id writeCNEMap(ChildNodeEntriesMap map) throws Exception { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - map.serialize(new BinaryBinding(out)); - byte[] bytes = out.toByteArray(); - Id id = new Id(idFactory.createContentId(bytes)); - writeFile(id, bytes); - return id; - } - - //-------------------------------------------------------< implementation > - - private File getFile(Id id) { - String sId = id.toString(); - StringBuilder buf = new StringBuilder(sId.substring(0, 2)); - buf.append('/'); - buf.append(sId.substring(2)); - return new File(dataDir, buf.toString()); - } - - private void writeFile(Id id, byte[] data) throws Exception { - File tmp = File.createTempFile("tmp", null, dataDir); - - try { - FileOutputStream fos = new FileOutputStream(tmp); - - try { - fos.write(data); - } finally { - //fos.getChannel().force(true); - fos.close(); - } - - File dst = getFile(id); - if (dst.exists()) { - // already exists - return; - } - // move tmp file - tmp.setReadOnly(); - if (tmp.renameTo(dst)) { - return; - } - // make sure parent dir exists and try again - dst.getParentFile().mkdir(); - if (tmp.renameTo(dst)) { - return; - } - throw new Exception("failed to create " + dst); - } finally { - tmp.delete(); - } - } -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/persistence/InMemPersistence.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/store/persistence/InMemPersistence.java deleted file mode 100644 index 66a43f6fd6e..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/persistence/InMemPersistence.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.store.persistence; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.InputStream; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import org.apache.jackrabbit.mk.blobs.BlobStore; -import org.apache.jackrabbit.mk.blobs.MemoryBlobStore; -import org.apache.jackrabbit.mk.model.ChildNodeEntriesMap; -import org.apache.jackrabbit.mk.model.Commit; -import org.apache.jackrabbit.mk.model.Id; -import org.apache.jackrabbit.mk.model.Node; -import org.apache.jackrabbit.mk.model.StoredCommit; -import org.apache.jackrabbit.mk.store.BinaryBinding; -import org.apache.jackrabbit.mk.store.Binding; -import org.apache.jackrabbit.mk.store.IdFactory; -import org.apache.jackrabbit.mk.store.NotFoundException; - -/** - * - */ -public class InMemPersistence implements Persistence, BlobStore { - - private final Map nodes = Collections.synchronizedMap(new HashMap()); - private final Map commits = Collections.synchronizedMap(new HashMap()); - private final Map cneMaps = Collections.synchronizedMap(new HashMap()); - private final BlobStore blobs = new MemoryBlobStore(); - - private Id head; - - // TODO: make this configurable - private IdFactory idFactory = IdFactory.getDigestFactory(); - - public void initialize(File homeDir) throws Exception { - head = null; - } - - public void close() { - } - - public Id readHead() throws Exception { - return head; - } - - public void writeHead(Id id) throws Exception { - head = id; - } - - public Binding readNodeBinding(Id id) throws NotFoundException, Exception { - byte[] bytes = nodes.get(id); - if (bytes != null) { - return new BinaryBinding(new ByteArrayInputStream(bytes)); - } else { - throw new NotFoundException(id.toString()); - } - } - - public Id writeNode(Node node) throws Exception { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - node.serialize(new BinaryBinding(out)); - byte[] bytes = out.toByteArray(); - Id id = new Id(idFactory.createContentId(bytes)); - - if (!nodes.containsKey(id)) { - nodes.put(id, bytes); - } - - return id; - } - - public StoredCommit readCommit(Id id) throws NotFoundException, Exception { - StoredCommit commit = commits.get(id); - if (commit != null) { - return commit; - } else { - throw new NotFoundException(id.toString()); - } - } - - public void writeCommit(Id id, Commit commit) throws Exception { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - commit.serialize(new BinaryBinding(out)); - byte[] bytes = out.toByteArray(); - - if (!commits.containsKey(id)) { - commits.put(id, StoredCommit.deserialize(id, new BinaryBinding(new ByteArrayInputStream(bytes)))); - } - } - - public ChildNodeEntriesMap readCNEMap(Id id) throws NotFoundException, Exception { - ChildNodeEntriesMap map = cneMaps.get(id); - if (map != null) { - return map; - } else { - throw new NotFoundException(id.toString()); - } - } - - public Id writeCNEMap(ChildNodeEntriesMap map) throws Exception { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - map.serialize(new BinaryBinding(out)); - byte[] bytes = out.toByteArray(); - Id id = new Id(idFactory.createContentId(bytes)); - - if (!cneMaps.containsKey(id)) { - cneMaps.put(id, ChildNodeEntriesMap.deserialize(new BinaryBinding(new ByteArrayInputStream(bytes)))); - } - - return id; - } - - //------------------------------------------------------------< BlobStore > - - public String addBlob(String tempFilePath) throws Exception { - return blobs.addBlob(tempFilePath); - } - - public String writeBlob(InputStream in) throws Exception { - return blobs.writeBlob(in); - } - - public int readBlob(String blobId, long pos, byte[] buff, int off, int length) throws NotFoundException, Exception { - return blobs.readBlob(blobId, pos, buff, off, length); - } - - public long getBlobLength(String blobId) throws Exception { - return blobs.getBlobLength(blobId); - } -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/persistence/MongoPersistence.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/store/persistence/MongoPersistence.java deleted file mode 100644 index fcba0930a2e..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/persistence/MongoPersistence.java +++ /dev/null @@ -1,478 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.store.persistence; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.InputStream; -import java.util.Iterator; - -import org.apache.jackrabbit.mk.blobs.BlobStore; -import org.apache.jackrabbit.mk.fs.FilePath; -import org.apache.jackrabbit.mk.model.ChildNodeEntriesMap; -import org.apache.jackrabbit.mk.model.Commit; -import org.apache.jackrabbit.mk.model.Id; -import org.apache.jackrabbit.mk.model.Node; -import org.apache.jackrabbit.mk.model.StoredCommit; -import org.apache.jackrabbit.mk.store.BinaryBinding; -import org.apache.jackrabbit.mk.store.Binding; -import org.apache.jackrabbit.mk.store.IdFactory; -import org.apache.jackrabbit.mk.store.NotFoundException; -import org.apache.jackrabbit.mk.util.ExceptionFactory; -import org.apache.jackrabbit.mk.util.IOUtils; -import org.apache.jackrabbit.mk.util.StringUtils; -import org.bson.types.ObjectId; - -import com.mongodb.BasicDBObject; -import com.mongodb.DB; -import com.mongodb.DBCollection; -import com.mongodb.DBObject; -import com.mongodb.Mongo; -import com.mongodb.MongoException; -import com.mongodb.WriteConcern; -import com.mongodb.gridfs.GridFS; -import com.mongodb.gridfs.GridFSDBFile; -import com.mongodb.gridfs.GridFSInputFile; - -/** - * - */ -public class MongoPersistence implements Persistence, BlobStore { - - private static final boolean BINARY_FORMAT = false; - - private static final String HEAD_COLLECTION = "head"; - private static final String NODES_COLLECTION = "nodes"; - private static final String COMMITS_COLLECTION = "commits"; - private static final String CNEMAPS_COLLECTION = "cneMaps"; - private static final String ID_FIELD = ":id"; - private static final String DATA_FIELD = ":data"; - - private Mongo con; - private DB db; - private DBCollection nodes; - private DBCollection commits; - private DBCollection cneMaps; - private GridFS fs; - - // TODO: make this configurable - private IdFactory idFactory = IdFactory.getDigestFactory(); - - public void initialize(File homeDir) throws Exception { - con = new Mongo(); - //con = new Mongo("localhost", 27017); - - db = con.getDB("mk"); - db.setWriteConcern(WriteConcern.SAFE); - - if (!db.collectionExists(HEAD_COLLECTION)) { - // capped collection of size 1 - db.createCollection(HEAD_COLLECTION, new BasicDBObject("capped", true).append("size", 256).append("max", 1)); - } - - nodes = db.getCollection(NODES_COLLECTION); - nodes.ensureIndex( - new BasicDBObject(ID_FIELD, 1), - new BasicDBObject("unique", true)); - - commits = db.getCollection(COMMITS_COLLECTION); - commits.ensureIndex( - new BasicDBObject(ID_FIELD, 1), - new BasicDBObject("unique", true)); - - cneMaps = db.getCollection(CNEMAPS_COLLECTION); - cneMaps.ensureIndex( - new BasicDBObject(ID_FIELD, 1), - new BasicDBObject("unique", true)); - - fs = new GridFS(db); - } - - public void close() { - con.close(); - con = null; - db = null; - } - - public Id readHead() throws Exception { - DBObject entry = db.getCollection(HEAD_COLLECTION).findOne(); - if (entry == null) { - return null; - } - return new Id((byte[]) entry.get(ID_FIELD)); - } - - public void writeHead(Id id) throws Exception { - // capped collection of size 1 - db.getCollection(HEAD_COLLECTION).insert(new BasicDBObject(ID_FIELD, id.getBytes())); - } - - public Binding readNodeBinding(Id id) throws NotFoundException, Exception { - BasicDBObject key = new BasicDBObject(); - if (BINARY_FORMAT) { - key.put(ID_FIELD, id.getBytes()); - } else { - key.put(ID_FIELD, id.toString()); - } - final BasicDBObject nodeObject = (BasicDBObject) nodes.findOne(key); - if (nodeObject != null) { - if (BINARY_FORMAT) { - byte[] bytes = (byte[]) nodeObject.get(DATA_FIELD); - return new BinaryBinding(new ByteArrayInputStream(bytes)); - } else { - return new DBObjectBinding(nodeObject); - } - } else { - throw new NotFoundException(id.toString()); - } - } - - public Id writeNode(Node node) throws Exception { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - node.serialize(new BinaryBinding(out)); - byte[] bytes = out.toByteArray(); - Id id = new Id(idFactory.createContentId(bytes)); - - BasicDBObject nodeObject; - if (BINARY_FORMAT) { - nodeObject = new BasicDBObject(ID_FIELD, id.getBytes()).append(DATA_FIELD, bytes); - } else { - nodeObject = new BasicDBObject(ID_FIELD, id.toString()); - node.serialize(new DBObjectBinding(nodeObject)); - } - try { - nodes.insert(nodeObject); - } catch (MongoException.DuplicateKey ignore) { - // fall through - } - - return id; - } - - public StoredCommit readCommit(Id id) throws NotFoundException, Exception { - BasicDBObject key = new BasicDBObject(); - - if (BINARY_FORMAT) { - key.put(ID_FIELD, id.getBytes()); - } else { - key.put(ID_FIELD, id.toString()); - } - BasicDBObject commitObject = (BasicDBObject) commits.findOne(key); - if (commitObject != null) { - if (BINARY_FORMAT) { - byte[] bytes = (byte[]) commitObject.get(DATA_FIELD); - return StoredCommit.deserialize(id, new BinaryBinding(new ByteArrayInputStream(bytes))); - } else { - return StoredCommit.deserialize(id, new DBObjectBinding(commitObject)); - } - } else { - throw new NotFoundException(id.toString()); - } - } - - public void writeCommit(Id id, Commit commit) throws Exception { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - commit.serialize(new BinaryBinding(out)); - byte[] bytes = out.toByteArray(); - - BasicDBObject commitObject; - if (BINARY_FORMAT) { - commitObject = new BasicDBObject(ID_FIELD, id.getBytes()).append(DATA_FIELD, bytes); - } else { - commitObject = new BasicDBObject(ID_FIELD, id.toString()); - commit.serialize(new DBObjectBinding(commitObject)); - } - try { - commits.insert(commitObject); - } catch (MongoException.DuplicateKey ignore) { - // fall through - } - } - - public ChildNodeEntriesMap readCNEMap(Id id) throws NotFoundException, Exception { - BasicDBObject key = new BasicDBObject(); - if (BINARY_FORMAT) { - key.put(ID_FIELD, id.getBytes()); - } else { - key.put(ID_FIELD, id.toString()); - } - BasicDBObject mapObject = (BasicDBObject) cneMaps.findOne(key); - if (mapObject != null) { - if (BINARY_FORMAT) { - byte[] bytes = (byte[]) mapObject.get(DATA_FIELD); - return ChildNodeEntriesMap.deserialize(new BinaryBinding(new ByteArrayInputStream(bytes))); - } else { - return ChildNodeEntriesMap.deserialize(new DBObjectBinding(mapObject)); - } - } else { - throw new NotFoundException(id.toString()); - } - } - - public Id writeCNEMap(ChildNodeEntriesMap map) throws Exception { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - map.serialize(new BinaryBinding(out)); - byte[] bytes = out.toByteArray(); - Id id = new Id(idFactory.createContentId(bytes)); - - BasicDBObject mapObject; - if (BINARY_FORMAT) { - mapObject = new BasicDBObject(ID_FIELD, id.getBytes()).append(DATA_FIELD, bytes); - } else { - mapObject = new BasicDBObject(ID_FIELD, id.toString()); - map.serialize(new DBObjectBinding(mapObject)); - } - try { - cneMaps.insert(mapObject); - } catch (MongoException.DuplicateKey ignore) { - // fall through - } - - return id; - } - - //------------------------------------------------------------< BlobStore > - - public String addBlob(String tempFilePath) throws Exception { - try { - FilePath file = FilePath.get(tempFilePath); - try { - InputStream in = file.newInputStream(); - return writeBlob(in); - } finally { - file.delete(); - } - } catch (Exception e) { - throw ExceptionFactory.convert(e); - } - } - - public String writeBlob(InputStream in) throws Exception { - GridFSInputFile f = fs.createFile(in, true); - //f.save(0x20000); // save in 128k chunks - f.save(); - - return f.getId().toString(); - } - - public int readBlob(String blobId, long pos, byte[] buff, int off, - int length) throws Exception { - - GridFSDBFile f = fs.findOne(new ObjectId(blobId)); - if (f == null) { - throw new NotFoundException(blobId); - } - // todo provide a more efficient implementation - InputStream in = f.getInputStream(); - try { - in.skip(pos); - return in.read(buff, off, length); - } finally { - IOUtils.closeQuietly(in); - } - } - - public long getBlobLength(String blobId) throws Exception { - GridFSDBFile f = fs.findOne(new ObjectId(blobId)); - if (f == null) { - throw new NotFoundException(blobId); - } - - return f.getLength(); - } - - //-------------------------------------------------------< implementation > - - protected final static String ENCODED_DOT = "_x46_"; - protected final static String ENCODED_DOLLAR_SIGN = "_x36_"; - - /** - * see http://www.mongodb.org/display/DOCS/Legal+Key+Names - * - * @param name - * @return - */ - protected static String encodeName(String name) { - StringBuilder buf = null; - for (int i = 0; i < name.length(); i++) { - if (i == 0 && name.charAt(i) == '$') { - // mongodb field names must not start with '$' - buf = new StringBuilder(); - buf.append(ENCODED_DOLLAR_SIGN); - } else if (name.charAt(i) == '.') { - // . is a reserved char for mongodb field names - if (buf == null) { - buf = new StringBuilder(name.substring(0, i)); - } - buf.append(ENCODED_DOT); - } else { - if (buf != null) { - buf.append(name.charAt(i)); - } - } - } - - return buf == null ? name : buf.toString(); - } - - protected static String decodeName(String name) { - StringBuilder buf = null; - - int lastPos = 0; - if (name.startsWith(ENCODED_DOLLAR_SIGN)) { - buf = new StringBuilder("$"); - lastPos = ENCODED_DOLLAR_SIGN.length(); - } - - int pos; - while ((pos = name.indexOf(ENCODED_DOT, lastPos)) != -1) { - if (buf == null) { - buf = new StringBuilder(); - } - buf.append(name.substring(lastPos, pos)); - buf.append('.'); - lastPos = pos + ENCODED_DOT.length(); - } - - if (buf != null) { - buf.append(name.substring(lastPos)); - return buf.toString(); - } else { - return name; - } - } - - //--------------------------------------------------------< inner classes > - - protected class DBObjectBinding implements Binding { - - BasicDBObject obj; - - protected DBObjectBinding(BasicDBObject obj) { - this.obj = obj; - } - - @Override - public void write(String key, String value) throws Exception { - obj.append(encodeName(key), value); - } - - @Override - public void write(String key, byte[] value) throws Exception { - obj.append(encodeName(key), StringUtils.convertBytesToHex(value)); - } - - @Override - public void write(String key, long value) throws Exception { - obj.append(encodeName(key), value); - } - - @Override - public void write(String key, int value) throws Exception { - obj.append(encodeName(key), value); - } - - @Override - public void writeMap(String key, int count, StringEntryIterator iterator) throws Exception { - BasicDBObject childObj = new BasicDBObject(); - while (iterator.hasNext()) { - StringEntry entry = iterator.next(); - childObj.append(encodeName(entry.getKey()), entry.getValue()); - } - obj.append(encodeName(key), childObj); - } - - @Override - public void writeMap(String key, int count, BytesEntryIterator iterator) throws Exception { - BasicDBObject childObj = new BasicDBObject(); - while (iterator.hasNext()) { - BytesEntry entry = iterator.next(); - childObj.append(encodeName(entry.getKey()), StringUtils.convertBytesToHex(entry.getValue())); - } - obj.append(encodeName(key), childObj); - } - - @Override - public String readStringValue(String key) throws Exception { - return obj.getString(encodeName(key)); - } - - @Override - public byte[] readBytesValue(String key) throws Exception { - return StringUtils.convertHexToBytes(obj.getString(encodeName(key))); - } - - @Override - public long readLongValue(String key) throws Exception { - return obj.getLong(encodeName(key)); - } - - @Override - public int readIntValue(String key) throws Exception { - return obj.getInt(encodeName(key)); - } - - @Override - public StringEntryIterator readStringMap(String key) throws Exception { - final BasicDBObject childObj = (BasicDBObject) obj.get(encodeName(key)); - final Iterator it = childObj.keySet().iterator(); - return new StringEntryIterator() { - @Override - public boolean hasNext() { - return it.hasNext(); - } - - @Override - public StringEntry next() { - String key = it.next(); - return new StringEntry(decodeName(key), childObj.getString(key)); - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - }; - } - - @Override - public BytesEntryIterator readBytesMap(String key) throws Exception { - final BasicDBObject childObj = (BasicDBObject) obj.get(encodeName(key)); - final Iterator it = childObj.keySet().iterator(); - return new BytesEntryIterator() { - @Override - public boolean hasNext() { - return it.hasNext(); - } - - @Override - public BytesEntry next() { - String key = it.next(); - return new BytesEntry( - decodeName(key), - StringUtils.convertHexToBytes(childObj.getString(key))); - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - }; - } - } -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/BloomFilterUtils.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/util/BloomFilterUtils.java deleted file mode 100644 index 8d00ea3abfa..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/BloomFilterUtils.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.util; - -/** - * Bloom filter utilities. - */ -public class BloomFilterUtils { - - /** - * The multiply and shift constants for the supplemental hash function. - */ - private static final int MUL = 2153, SHIFT = 19; - - /** - * The number of bits needed per stored element. - * Using the formula m = - (n * ln(p)) / (ln(2)^2) as described in - * http://en.wikipedia.org/wiki/Bloom_filter - * (simplified, as we used a fixed K: 2). - */ - private static final double BIT_FACTOR = -Math.log(0.02) / Math.pow(Math.log(2), 2); - - /** - * Create a bloom filter array for the given number of elements. - * - * @param count the number of entries - * @param maxBytes the maximum number of bytes - * @return the empty bloom filter - */ - public static byte[] createFilter(int elementCount, int maxBytes) { - int bits = (int) (elementCount * BIT_FACTOR) + 7; - return new byte[Math.min(maxBytes, bits / 8)]; - } - - /** - * Add the key. - * - * @param bloom the bloom filter - * @param key the key - */ - public static void add(byte[] bloom, Object key) { - int len = bloom.length; - if (len > 0) { - int h1 = hash(key.hashCode()), h2 = hash(h1); - bloom[(h1 >>> 3) % len] |= 1 << (h1 & 7); - bloom[(h2 >>> 3) % len] |= 1 << (h2 & 7); - } - } - - /** - * Check whether the given key is probably in the set. This method never - * returns false if the key is in the set, but possibly returns true even if - * it isn't. - * - * @param bloom the bloom filter - * @param key the key - * @return true if the given key is probably in the set - */ - public static boolean probablyContains(byte[] bloom, Object key) { - int len = bloom.length; - if (len == 0) { - return true; - } - int h1 = hash(key.hashCode()), h2 = hash(h1); - int x = bloom[(h1 >>> 3) % len] & (1 << (h1 & 7)); - if (x != 0) { - x = bloom[(h2 >>> 3) % len] & (1 << (h2 & 7)); - } - return x != 0; - } - - /** - * Get the hash value for the given key. The returned hash value is - * stretched so that it should work well even for relatively bad hashCode - * implementations. - * - * @param key the key - * @return the hash value - */ - private static int hash(int oldHash) { - return oldHash ^ ((oldHash * MUL) >> SHIFT); - } - -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/MemorySockets.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/util/MemorySockets.java deleted file mode 100644 index 5db58237d48..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/MemorySockets.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.util; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InterruptedIOException; -import java.io.OutputStream; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.UnknownHostException; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.atomic.AtomicBoolean; - -import javax.net.ServerSocketFactory; -import javax.net.SocketFactory; - -/** - * Memory sockets. - */ -public abstract class MemorySockets { - - /** Sockets queue */ - static final BlockingQueue QUEUE = new LinkedBlockingQueue(); - - /** Sentinel socket, used to signal a closed queue */ - static final Socket SENTINEL = new Socket(); - - /** - * Return the server socket factory. - * - * @return server socket factory - */ - public static ServerSocketFactory getServerSocketFactory() { - return new ServerSocketFactory() { - @Override - public ServerSocket createServerSocket() throws IOException { - return new ServerSocket() { - /** Closed flag */ - private final AtomicBoolean closed = new AtomicBoolean(); - - @Override - public Socket accept() throws IOException { - if (closed.get()) { - throw new IOException("closed"); - } - try { - Socket socket = QUEUE.take(); - if (socket == SENTINEL) { - throw new IOException("closed"); - } - return socket; - } catch (InterruptedException e) { - throw new InterruptedIOException(); - } - } - - @Override - public void close() throws IOException { - if (closed.compareAndSet(false, true)) { - QUEUE.add(SENTINEL); - } - } - }; - } - - @Override - public ServerSocket createServerSocket(int port) throws IOException { - return createServerSocket(); - } - - @Override - public ServerSocket createServerSocket(int port, int backlog) - throws IOException { - - return createServerSocket(); - } - - @Override - public ServerSocket createServerSocket(int port, int backlog, - InetAddress ifAddress) throws IOException { - - return createServerSocket(); - } - }; - } - - /** - * Return the socket factory. - * - * @return socket factory - */ - public static SocketFactory getSocketFactory() { - return new SocketFactory() { - @Override - public Socket createSocket() throws IOException { - PipedSocket socket = new PipedSocket(); - QUEUE.add(new PipedSocket(socket)); - return socket; - } - - @Override - public Socket createSocket(InetAddress host, int port) throws IOException { - return createSocket(); - } - - @Override - public Socket createSocket(String host, int port) throws IOException, - UnknownHostException { - - return createSocket(); - } - - @Override - public Socket createSocket(String host, int port, InetAddress localHost, - int localPort) throws IOException, UnknownHostException { - - return createSocket(); - } - - @Override - public Socket createSocket(InetAddress address, int port, - InetAddress localAddress, int localPort) throws IOException { - - return createSocket(); - } - }; - }; - - /** - * Socket implementation, using pipes to exchange information between a - * pair of sockets. - */ - static class PipedSocket extends Socket { - - /** Input stream */ - protected final PipedInputStream in; - - /** Output stream */ - protected final PipedOutputStream out; - - /** - * Used to initialize the socket on the client side. - */ - PipedSocket() { - in = new PipedInputStream(8192); - out = new PipedOutputStream(); - } - - /** - * Used to initialize the socket on the server side. - */ - PipedSocket(PipedSocket client) throws IOException { - in = new PipedInputStream(client.out); - out = new PipedOutputStream(client.in); - } - - @Override - public InputStream getInputStream() throws IOException { - return in; - } - - @Override - public OutputStream getOutputStream() throws IOException { - return out; - } - } -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/Sync.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/util/Sync.java deleted file mode 100644 index d5fc9ac5d14..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/Sync.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.util; - -import java.util.Iterator; -import org.apache.jackrabbit.mk.api.MicroKernel; -import org.apache.jackrabbit.mk.simple.NodeImpl; - -/** - * Traverse the nodes in two repositories / revisions / nodes in order to - * synchronize them or list the differences. - *

- * If the target is not set, the tool can be used to list or backup the content, - * for (data store) garbage collection, or similar. - */ -public class Sync { - - private MicroKernel sourceMk, targetMk; - private String sourceRev, targetRev; - private String sourcePath, targetPath = "/"; - private boolean useContentHashOptimization; - private int childNodesPerBatch = 100; - - private Handler handler; - - /** - * Set the source (required). - * - * @param mk the source - * @param rev the revision - * @param path the path - */ - public void setSource(MicroKernel mk, String rev, String path) { - sourceMk = mk; - sourceRev = rev; - sourcePath = path; - } - - /** - * Set the target (optional). If not set, the tool assumes no nodes exist on - * the target. - * - * @param mk the target - * @param rev the revision - * @param path the path - */ - - public void setTarget(MicroKernel mk, String rev, String path) { - targetMk = mk; - targetRev = rev; - targetPath = path; - } - - /** - * Whether to use the content hash optimization if available. - * - * @return true if the optimization should be used - */ - public boolean getUseContentHashOptimization() { - return useContentHashOptimization; - } - - /** - * Use the content hash optimization if available. - * - * @param useContentHashOptimization the new value - */ - public void setUseContentHashOptimization(boolean useContentHashOptimization) { - this.useContentHashOptimization = useContentHashOptimization; - } - - /** - * Get the number of child nodes to request in one call. - * - * @return the number of child nodes to request - */ - public int getChildNodesPerBatch() { - return childNodesPerBatch; - } - - /** - * Set the number of child nodes to request in one call. - * - * @param childNodesPerBatch the number of child nodes to request - */ - public void setChildNodesPerBatch(int childNodesPerBatch) { - this.childNodesPerBatch = childNodesPerBatch; - } - - public void run(Handler handler) { - this.handler = handler; - visit(""); - } - - public void visit(String relPath) { - String source = PathUtils.concat(sourcePath, relPath); - String target = PathUtils.concat(targetPath, relPath); - NodeImpl s = null, t = null; - if (sourceMk.nodeExists(source, sourceRev)) { - s = NodeImpl.parse(sourceMk.getNodes(source, sourceRev, 0, 0, childNodesPerBatch, null)); - } - if (targetMk != null && targetMk.nodeExists(target, targetRev)) { - t = NodeImpl.parse(targetMk.getNodes(target, targetRev, 0, 0, childNodesPerBatch, null)); - } - if (s == null || t == null) { - if (s == t) { - // both don't exist - ok - return; - } else if (s == null) { - handler.removeNode(target); - return; - } else { - if (!PathUtils.denotesRoot(target)) { - handler.addNode(target); - } - } - } - // properties - for (int i = 0; i < s.getPropertyCount(); i++) { - String name = s.getProperty(i); - String sourceValue = s.getPropertyValue(i); - String targetValue = t != null && t.hasProperty(name) ? t.getProperty(name) : null; - if (!sourceValue.equals(targetValue)) { - handler.setProperty(target, name, sourceValue); - } - } - if (t != null) { - for (int i = 0; i < t.getPropertyCount(); i++) { - String name = t.getProperty(i); - // if it exists in the source, it's already updated - if (!s.hasProperty(name)) { - handler.setProperty(target, name, null); - } - } - } - // child nodes - Iterator it = s.getTotalChildNodeCount() > s.getChildNodeCount() ? - getAllChildNodeNames(sourceMk, source, sourceRev, childNodesPerBatch) : - s.getChildNodeNames(Integer.MAX_VALUE); - while (it.hasNext()) { - String name = it.next(); - visit(PathUtils.concat(relPath, name)); - } - if (t != null) { - it = t.getTotalChildNodeCount() > t.getChildNodeCount() ? - getAllChildNodeNames(targetMk, target, targetRev, childNodesPerBatch) : - t.getChildNodeNames(Integer.MAX_VALUE); - while (it.hasNext()) { - String name = it.next(); - if (s.exists(name)) { - // if it exists in the source, it's already updated - } else if (s.getTotalChildNodeCount() > s.getChildNodeCount() && - sourceMk.nodeExists(PathUtils.concat(source, name), sourceRev)) { - // if it exists in the source, it's already updated - // (in this case, there are many child nodes) - } else { - visit(PathUtils.concat(relPath, name)); - } - } - } - } - - /** - * Get a child node name iterator that batches node names. This work - * efficiently for small and big child node lists. - * - * @param mk the implementation - * @param path the path - * @param rev the revision - * @param batchSize the batch size - * @return a child node name iterator - */ - public static Iterator getAllChildNodeNames(final MicroKernel mk, final String path, final String rev, final int batchSize) { - return new Iterator() { - - private long offset; - private Iterator current; - - { - nextBatch(); - } - - private void nextBatch() { - NodeImpl n = NodeImpl.parse(mk.getNodes(path, rev, 0, offset, batchSize, null)); - current = n.getChildNodeNames(Integer.MAX_VALUE); - offset += batchSize; - } - - @Override - public boolean hasNext() { - if (!current.hasNext()) { - nextBatch(); - } - return current.hasNext(); - } - - @Override - public String next() { - if (!current.hasNext()) { - nextBatch(); - } - return current.next(); - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - }; - } - - /** - * The sync handler. - */ - public interface Handler { - - /** - * The given node needs to be added to the target. - * - * @param path the path - */ - void addNode(String path); - - /** - * The given node needs to be removed from the target. - * - * @param path the path - */ - void removeNode(String target); - - /** - * The given property needs to be set on the target. - * - * @param path the path - * @param property the property name - * @param value the new value, or null to remove it - */ - void setProperty(String target, String property, String value); - - } - -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/SynchronizedVerifier.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/util/SynchronizedVerifier.java deleted file mode 100644 index 96333146532..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/SynchronizedVerifier.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.util; - -import java.util.Collections; -import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * A utility class that allows to verify access to a resource is synchronized. - */ -public class SynchronizedVerifier { - - private static volatile boolean enabled; - private static final Map, AtomicBoolean> DETECT = - Collections.synchronizedMap(new HashMap, AtomicBoolean>()); - private static final Map CURRENT = - Collections.synchronizedMap(new IdentityHashMap()); - - /** - * Enable or disable detection for a given class. - * - * @param clazz the class - * @param value the new value (true means detection is enabled) - */ - public static void setDetect(Class clazz, boolean value) { - if (value) { - DETECT.put(clazz, new AtomicBoolean()); - } else { - AtomicBoolean b = DETECT.remove(clazz); - if (b == null) { - throw new AssertionError("Detection was not enabled"); - } else if (!b.get()) { - throw new AssertionError("No object of this class was tested"); - } - } - enabled = DETECT.size() > 0; - } - - /** - * Verify the object is not accessed concurrently. - * - * @param o the object - * @param write if the object is modified - */ - public static void check(Object o, boolean write) { - if (enabled) { - detectConcurrentAccess(o, write); - } - } - - private static void detectConcurrentAccess(Object o, boolean write) { - AtomicBoolean value = DETECT.get(o.getClass()); - if (value != null) { - value.set(true); - Boolean old = CURRENT.put(o, write); - if (old != null) { - if (write || old) { - throw new AssertionError("Concurrent write access"); - } - } - try { - Thread.sleep(1); - } catch (InterruptedException e) { - // ignore - } - old = CURRENT.remove(o); - if (old == null && write) { - throw new AssertionError("Concurrent write access"); - } - } - } - -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/wrapper/IndexWrapper.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/wrapper/IndexWrapper.java deleted file mode 100644 index d8a98b61f1a..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/wrapper/IndexWrapper.java +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.wrapper; - -import java.io.InputStream; -import java.util.HashMap; -import java.util.Iterator; -import org.apache.jackrabbit.mk.MicroKernelFactory; -import org.apache.jackrabbit.mk.api.MicroKernel; -import org.apache.jackrabbit.mk.api.MicroKernelException; -import org.apache.jackrabbit.mk.index.Indexer; -import org.apache.jackrabbit.mk.index.PrefixIndex; -import org.apache.jackrabbit.mk.index.PropertyIndex; -import org.apache.jackrabbit.mk.json.JsopReader; -import org.apache.jackrabbit.mk.json.JsopStream; -import org.apache.jackrabbit.mk.json.JsopTokenizer; -import org.apache.jackrabbit.mk.simple.NodeImpl; -import org.apache.jackrabbit.mk.simple.NodeMap; -import org.apache.jackrabbit.mk.util.ExceptionFactory; -import org.apache.jackrabbit.mk.util.PathUtils; - -/** - * The index mechanism, as a wrapper. - */ -public class IndexWrapper extends WrapperBase implements MicroKernel { - - private static final String INDEX_PATH = "/index"; - private static final String TYPE_PREFIX = "prefix:"; - private static final String TYPE_PROPERTY = "property:"; - private static final String UNIQUE = "unique"; - - private final Wrapper mk; - private final Indexer indexer; - private final NodeMap map = new NodeMap(); - private final HashMap prefixIndexes = new HashMap(); - private final HashMap propertyIndexes = new HashMap(); - - public IndexWrapper(MicroKernel mk) { - this.mk = WrapperBase.wrap(mk); - this.indexer = new Indexer(mk); - } - - public static synchronized IndexWrapper get(String url) { - String u = url.substring("index:".length()); - IndexWrapper w = new IndexWrapper(MicroKernelFactory.getInstance(u)); - return w; - } - - public String getHeadRevision() { - return mk.getHeadRevision(); - } - - public long getLength(String blobId) { - return mk.getLength(blobId); - } - - public boolean nodeExists(String path, String revisionId) { - return mk.nodeExists(path, revisionId); - } - - public long getChildNodeCount(String path, String revisionId) { - return mk.getChildNodeCount(path, revisionId); - } - - public int read(String blobId, long pos, byte[] buff, int off, int length) { - return mk.read(blobId, pos, buff, off, length); - } - - public String waitForCommit(String oldHeadRevision, long maxWaitMillis) throws MicroKernelException, InterruptedException { - return mk.waitForCommit(oldHeadRevision, maxWaitMillis); - } - - public String write(InputStream in) { - return mk.write(in); - } - - public String commitStream(String rootPath, JsopReader jsonDiff, String revisionId, String message) { - if (!rootPath.startsWith(INDEX_PATH)) { - String rev = mk.commitStream(rootPath, jsonDiff, revisionId, message); - jsonDiff.resetReader(); - indexer.updateIndex(rootPath, jsonDiff, rev); - rev = mk.getHeadRevision(); - rev = indexer.updateEnd(rev); - return rev; - } - JsopReader t = jsonDiff; - while (true) { - int r = t.read(); - if (r == JsopTokenizer.END) { - break; - } - String path; - if (rootPath == null) { - path = t.readString(); - } else { - path = PathUtils.concat(rootPath, t.readString()); - } - switch (r) { - case '+': - t.read(':'); - t.read('{'); - // parse but ignore - NodeImpl.parse(map, t, 0); - path = PathUtils.relativize(INDEX_PATH, path); - if (path.startsWith(TYPE_PREFIX)) { - String prefix = path.substring(TYPE_PREFIX.length()); - PrefixIndex idx = indexer.createPrefixIndex(prefix); - prefixIndexes.put(path, idx); - } else if (path.startsWith(TYPE_PROPERTY)) { - String property = path.substring(TYPE_PROPERTY.length()); - boolean unique = false; - if (property.endsWith("," + UNIQUE)) { - unique = true; - property = property.substring(0, property.length() - UNIQUE.length() - 1); - } - PropertyIndex idx = indexer.createPropertyIndex(property, unique); - propertyIndexes.put(path, idx); - } else { - throw ExceptionFactory.get("Unknown index type: " + path); - } - break; - case '-': - throw ExceptionFactory.get("Removing indexes is not yet implemented"); - default: - throw ExceptionFactory.get("token: " + (char) t.getTokenType()); - } - } - return null; - } - - public JsopReader getNodesStream(String path, String revisionId, int depth, long offset, int count, String filter) { - if (!path.startsWith(INDEX_PATH)) { - return mk.getNodesStream(path, revisionId, depth, offset, count, filter); - } - String index = PathUtils.relativize(INDEX_PATH, path); - int idx = index.indexOf('?'); - if (idx < 0) { - throw ExceptionFactory.get("Invalid query. Expected: /index/prefix:x?y, got: " + path); - } - String data = index.substring(idx + 1); - index = index.substring(0, idx); - JsopStream s = new JsopStream(); - s.array(); - if (index.startsWith(TYPE_PREFIX)) { - PrefixIndex prefixIndex = prefixIndexes.get(index); - if (prefixIndex == null) { - if (mk.nodeExists(path, mk.getHeadRevision())) { - prefixIndex = indexer.createPrefixIndex(index); - } else { - throw ExceptionFactory.get("Unknown index: " + index); - } - } - Iterator it = prefixIndex.getPaths(data, revisionId); - while (it.hasNext()) { - s.value(it.next()); - } - } else if (index.startsWith(TYPE_PROPERTY)) { - PropertyIndex propertyIndex = propertyIndexes.get(index); - boolean unique = index.endsWith("," + UNIQUE); - if (propertyIndex == null) { - if (mk.nodeExists(path, mk.getHeadRevision())) { - String indexName = index; - if (unique) { - indexName = index.substring(0, index.length() - UNIQUE.length() - 1); - } - propertyIndex = indexer.createPropertyIndex(indexName, unique); - } else { - throw ExceptionFactory.get("Unknown index: " + index); - } - } - if (unique) { - String value = propertyIndex.getPath(data, revisionId); - if (value != null) { - s.value(value); - } - } else { - Iterator it = propertyIndex.getPaths(data, revisionId); - while (it.hasNext()) { - s.value(it.next()); - } - } - } - s.endArray(); - return s; - } - - public JsopReader diffStream(String fromRevisionId, String toRevisionId, String filter) { - return mk.diffStream(fromRevisionId, toRevisionId, filter); - } - - public JsopReader getJournalStream(String fromRevisionId, String toRevisionId, String filter) { - return mk.getJournalStream(fromRevisionId, toRevisionId, filter); - } - - public JsopReader getNodesStream(String path, String revisionId) { - return getNodesStream(path, revisionId, 1, 0, -1, null); - } - - public JsopReader getRevisionsStream(long since, int maxEntries) { - return mk.getRevisionsStream(since, maxEntries); - } - - public void dispose() { - mk.dispose(); - } - -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/wrapper/WrapperBase.java b/oak-core/src/main/java/org/apache/jackrabbit/mk/wrapper/WrapperBase.java deleted file mode 100644 index 22782cad94a..00000000000 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/wrapper/WrapperBase.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.wrapper; - -import java.io.InputStream; -import org.apache.jackrabbit.mk.api.MicroKernel; -import org.apache.jackrabbit.mk.api.MicroKernelException; -import org.apache.jackrabbit.mk.json.JsopReader; -import org.apache.jackrabbit.mk.json.JsopTokenizer; - -public abstract class WrapperBase implements MicroKernel, Wrapper { - - public final String commit(String path, String jsonDiff, String revisionId, String message) { - return commitStream(path, new JsopTokenizer(jsonDiff), revisionId, message); - } - - public final String getJournal(String fromRevisionId, String toRevisionId, String filter) { - return getJournalStream(fromRevisionId, toRevisionId, filter).toString(); - } - - public final String getNodes(String path, String revisionId) { - return getNodesStream(path, revisionId).toString(); - } - - public final String getNodes(String path, String revisionId, int depth, long offset, int count, String filter) { - return getNodesStream(path, revisionId, depth, offset, count, filter).toString(); - } - - public final String diff(String fromRevisionId, String toRevisionId, String filter) { - return diffStream(fromRevisionId, toRevisionId, filter).toString(); - } - - public final String getRevisions(long since, int maxEntries) { - return getRevisionsStream(since, maxEntries).toString(); - } - - public static Wrapper wrap(final MicroKernel mk) { - if (mk instanceof Wrapper) { - return (Wrapper) mk; - } - return new Wrapper() { - - MicroKernel wrapped = mk; - - public String commitStream(String path, JsopReader jsonDiff, String revisionId, String message) { - return wrapped.commit(path, jsonDiff.toString(), revisionId, message); - } - - public JsopReader getJournalStream(String fromRevisionId, String toRevisionId, String filter) { - return new JsopTokenizer(wrapped.getJournal(fromRevisionId, toRevisionId, filter)); - } - - public JsopReader getNodesStream(String path, String revisionId) { - return new JsopTokenizer(wrapped.getNodes(path, revisionId)); - } - - public JsopReader getNodesStream(String path, String revisionId, int depth, long offset, int count, String filter) { - return new JsopTokenizer(wrapped.getNodes(path, revisionId, depth, offset, count, filter)); - } - - public JsopReader getRevisionsStream(long since, int maxEntries) { - return new JsopTokenizer(wrapped.getRevisions(since, maxEntries)); - } - - public JsopReader diffStream(String fromRevisionId, String toRevisionId, String path) { - return new JsopTokenizer(wrapped.diff(fromRevisionId, toRevisionId, path)); - } - - public String commit(String path, String jsonDiff, String revisionId, String message) { - return wrapped.commit(path, jsonDiff, revisionId, message); - } - - public String diff(String fromRevisionId, String toRevisionId, String path) { - return wrapped.diff(fromRevisionId, toRevisionId, path); - } - - public void dispose() { - wrapped.dispose(); - } - - public String getHeadRevision() throws MicroKernelException { - return wrapped.getHeadRevision(); - } - - public String getJournal(String fromRevisionId, String toRevisionId, String filter) { - return wrapped.getJournal(fromRevisionId, toRevisionId, filter); - } - - public long getLength(String blobId) { - return wrapped.getLength(blobId); - } - - public String getNodes(String path, String revisionId) { - return wrapped.getNodes(path, revisionId); - } - - public String getNodes(String path, String revisionId, int depth, long offset, int count, String filter) { - return wrapped.getNodes(path, revisionId, depth, offset, count, filter); - } - - public String getRevisions(long since, int maxEntries) { - return wrapped.getRevisions(since, maxEntries); - } - - public boolean nodeExists(String path, String revisionId) { - return wrapped.nodeExists(path, revisionId); - } - - public long getChildNodeCount(String path, String revisionId) { - return wrapped.getChildNodeCount(path, revisionId); - } - - public int read(String blobId, long pos, byte[] buff, int off, int length) { - return wrapped.read(blobId, pos, buff, off, length); - } - - public String waitForCommit(String oldHeadRevision, long maxWaitMillis) throws InterruptedException { - return wrapped.waitForCommit(oldHeadRevision, maxWaitMillis); - } - - public String write(InputStream in) { - return wrapped.write(in); - } - - }; - } - -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java new file mode 100644 index 00000000000..044a2efb68c --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java @@ -0,0 +1,253 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak; + +import java.util.List; +import javax.annotation.Nonnull; +import javax.jcr.NoSuchWorkspaceException; +import javax.security.auth.login.LoginException; + +import com.google.common.collect.Lists; +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +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.core.ContentRepositoryImpl; +import org.apache.jackrabbit.oak.kernel.KernelNodeStore; +import org.apache.jackrabbit.oak.spi.commit.CommitHook; +import org.apache.jackrabbit.oak.spi.commit.CompositeHook; +import org.apache.jackrabbit.oak.spi.commit.CompositeValidatorProvider; +import org.apache.jackrabbit.oak.spi.commit.ConflictHandler; +import org.apache.jackrabbit.oak.spi.commit.ValidatingHook; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; +import org.apache.jackrabbit.oak.spi.lifecycle.RepositoryInitializer; +import org.apache.jackrabbit.oak.spi.query.CompositeQueryIndexProvider; +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider; +import org.apache.jackrabbit.oak.spi.security.SecurityConfiguration; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Builder class for constructing {@link ContentRepository} instances with + * a set of specified plugin components. This class acts as a public facade + * that hides the internal implementation classes and the details of how + * they get instantiated and wired together. + * + * @since Oak 0.6 + */ +public class Oak { + + private static final Logger log = LoggerFactory.getLogger(Oak.class); + + private final MicroKernel kernel; + + private final List initializers = Lists.newArrayList(); + + private final List queryIndexProviders = Lists.newArrayList(); + + private final List commitHooks = Lists.newArrayList(); + + private List validatorProviders = Lists.newArrayList(); + + // TODO: review if we really want to have the OpenSecurityProvider as default. + private SecurityProvider securityProvider = new OpenSecurityProvider(); + + private ConflictHandler conflictHandler; + + public Oak(MicroKernel kernel) { + this.kernel = kernel; + } + + public Oak() { + this(new MicroKernelImpl()); + } + + @Nonnull + public Oak with(@Nonnull RepositoryInitializer initializer) { + initializers.add(checkNotNull(initializer)); + return this; + } + + /** + * Associates the given query index provider with the repository to + * be created. + * + * @param provider query index provider + * @return this builder + */ + @Nonnull + public Oak with(@Nonnull QueryIndexProvider provider) { + queryIndexProviders.add(provider); + return this; + } + + /** + * Associates the given commit hook with the repository to be created. + * + * @param hook commit hook + * @return this builder + */ + @Nonnull + public Oak with(@Nonnull CommitHook hook) { + withValidatorHook(); + commitHooks.add(hook); + return this; + } + + /** + * Turns all currently tracked validators to a validating commit hook + * and associates that hook with the repository to be created. This way + * a sequence of {@code with()} calls that alternates between validators + * and other commit hooks will have all the validators in the correct + * order while still being able to leverage the performance gains of + * multiple validators iterating over the changes simultaneously. + */ + private void withValidatorHook() { + if (!validatorProviders.isEmpty()) { + commitHooks.add(new ValidatingHook( + CompositeValidatorProvider.compose(validatorProviders))); + validatorProviders = Lists.newArrayList(); + } + } + + /** + * Associates the given validator provider with the repository to + * be created. + * + * @param provider validator provider + * @return this builder + */ + @Nonnull + public Oak with(@Nonnull ValidatorProvider provider) { + validatorProviders.add(provider); + return this; + } + + /** + * Associates the given validator with the repository to be created. + * + * @param validator validator + * @return this builder + */ + @Nonnull + public Oak with(@Nonnull final Validator validator) { + return with(new ValidatorProvider() { + @Override @Nonnull + public Validator getRootValidator( + NodeState before, NodeState after) { + return validator; + } + }); + } + + @Nonnull + public Oak with(@Nonnull SecurityProvider securityProvider) { + this.securityProvider = securityProvider; + try { + for (SecurityConfiguration sc : securityProvider.getSecurityConfigurations()) { + validatorProviders.addAll(sc.getValidatorProviders()); + } + } catch (UnsupportedOperationException e) { + log.info(e.getMessage()); + } + return this; + } + + /** + * Associates the given conflict handler with the repository to be created. + * + * @param conflictHandler conflict handler + * @return this builder + */ + @Nonnull + public Oak with(@Nonnull ConflictHandler conflictHandler) { + this.conflictHandler = conflictHandler; + return this; + } + + public ContentRepository createContentRepository() { + KernelNodeStore store = new KernelNodeStore(kernel); + for (RepositoryInitializer initializer : initializers) { + initializer.initialize(store); + } + + withValidatorHook(); + store.setHook(CompositeHook.compose(commitHooks)); + + return new ContentRepositoryImpl( + store, + conflictHandler, + CompositeQueryIndexProvider.compose(queryIndexProviders), + securityProvider); + } + + /** + * Creates a content repository with the given configuration + * and logs in to the default workspace with no credentials, + * returning the resulting content session. + *

+ * This method exists mostly as a convenience for one-off tests, + * as there's no way to create other sessions for accessing the + * same repository. + *

+ * There is typically no need to explicitly close the returned + * session unless the repository has explicitly been configured + * to reserve some resources until all sessions have been closed. + * The repository will be garbage collected once the session is no + * longer used. + * + * @return content session + */ + public ContentSession createContentSession() { + try { + return createContentRepository().login(null, null); + } catch (NoSuchWorkspaceException e) { + throw new IllegalStateException("Default workspace not found", e); + } catch (LoginException e) { + throw new IllegalStateException("Anonymous login not allowed", e); + } + } + + /** + * Creates a content repository with the given configuration + * and returns a {@link Root} instance after logging in to the + * default workspace with no credentials. + *

+ * This method exists mostly as a convenience for one-off tests, as + * the returned root is the only way to access the session or the + * repository. + *

+ * Note that since there is no way to close the underlying content + * session, this method should only be used when no components that + * require sessions to be closed have been configured. The repository + * and the session will be garbage collected once the root is no longer + * used. + * + * @return root instance + */ + public Root createRoot() { + return createContentSession().getLatestRoot(); + } + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/api/AuthInfo.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/AuthInfo.java new file mode 100644 index 00000000000..4518a1c637d --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/AuthInfo.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.api; + +import java.security.Principal; +import java.util.Collections; +import java.util.Set; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +/** + * {@code AuthInfo} instances provide access to information related + * to authentication and authorization of a given content session. + * {@code AuthInfo} instances are guaranteed to be immutable. + */ +public interface AuthInfo { + + AuthInfo EMPTY = new AuthInfo() { + @Override + public String getUserID() { + return null; + } + + @Nonnull + @Override + public String[] getAttributeNames() { + return new String[0]; + } + + @Override + public Object getAttribute(String attributeName) { + return null; + } + + @Override + public Set getPrincipals() { + return Collections.emptySet(); + } + }; + + /** + * Return the user ID to be exposed on the JCR Session object. It refers + * to the ID of the user associated with the Credentials passed to the + * repository login. + * + * @return the user ID such as exposed on the JCR Session object. + */ + @CheckForNull + String getUserID(); + + /** + * Returns the attribute names associated with this instance. + * + * @return The attribute names with that instance or an empty array if + * no attributes are present. + */ + @Nonnull + String[] getAttributeNames(); + + /** + * Returns the attribute with the given name or {@code null} if no attribute + * with that {@code attributeName} exists. + * + * @param attributeName The attribute name. + * @return The attribute or {@code null}. + */ + @CheckForNull + Object getAttribute(String attributeName); + + /** + * Returns the set of principals associated with this {@code AuthInfo} instance. + * + * @return A set of principals. + */ + Set getPrincipals(); +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Blob.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Blob.java new file mode 100644 index 00000000000..f7f4d4969e6 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Blob.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.api; + +import java.io.InputStream; + +import javax.annotation.Nonnull; + +/** + * Immutable representation of a binary value of finite length. + */ +public interface Blob extends Comparable { + + /** + * Returns a new stream for this value object. Multiple calls to this + * methods return equal instances: {@code getNewStream().equals(getNewStream())}. + * @return a new stream for this value based on an internal conversion. + */ + @Nonnull + InputStream getNewStream(); + + /** + * Returns the length of this blob or -1 if unknown. + * + * @return the length of this blob. + */ + long length(); + + /** + * The SHA-256 digest of the underlying stream + * @return + */ + byte[] sha256(); +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/api/BlobFactory.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/BlobFactory.java new file mode 100644 index 00000000000..2632da767af --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/BlobFactory.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.api; + +import java.io.IOException; +import java.io.InputStream; + +/** + * BlobFactory... + * TODO review again if we really need/want to expose that in the OAK API + * TODO in particular exposing this interface (and Blob) requires additional thoughts on + * TODO - lifecycle of the factory, + * TODO - lifecycle of the Blob, + * TODO - access restrictions and how permissions are enforced on blob creation + * TODO - searchability, versioning and so forth + */ +public interface BlobFactory { + + /** + * Create a {@link Blob} from the given input stream. The input stream + * is closed after this method returns. + * @param inputStream The input stream for the {@code Blob} + * @return The {@code Blob} representing {@code inputStream} + * @throws java.io.IOException If an error occurs while reading from the stream + */ + Blob createBlob(InputStream inputStream) throws IOException; +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/api/CommitFailedException.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/CommitFailedException.java new file mode 100644 index 00000000000..19d83b6e6df --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/CommitFailedException.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.api; + +import javax.jcr.RepositoryException; + +/** + * Main exception thrown by methods defined on the {@code ContentSession} interface + * indicating that committing a given set of changes failed. + */ +public class CommitFailedException extends Exception { + public CommitFailedException() { + } + + public CommitFailedException(String message) { + super(message); + } + + public CommitFailedException(String message, Throwable cause) { + super(message, cause); + } + + public CommitFailedException(Throwable cause) { + super(cause); + } + + /** + * Rethrow this exception cast into a {@link RepositoryException}: if the cause + * for this exception already is a {@code RepositoryException}, this exception is + * wrapped into the actual type of the cause and rethrown. If creating an instance + * of the actual type of the cause fails, cause is simply re-thrown. + * If the cause for this exception is not a {@code RepositoryException} then a + * new {@code RepositoryException} instance with this {@code CommitFailedException} + * is thrown. + * @throws RepositoryException + */ + public void throwRepositoryException() throws RepositoryException { + Throwable cause = getCause(); + if (cause instanceof RepositoryException) { + RepositoryException e; + try { + // Try to preserve all parts of the stack trace + e = (RepositoryException) cause.getClass().getConstructor().newInstance(); + e.initCause(this); + } + catch (Exception ex) { + // Fall back to the initial cause on failure + e = (RepositoryException) cause; + } + + throw e; + } + else { + throw new RepositoryException(this); + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/api/ContentRepository.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/ContentRepository.java new file mode 100644 index 00000000000..11063eec9a5 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/ContentRepository.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.api; + +import javax.annotation.Nonnull; +import javax.jcr.Credentials; +import javax.jcr.NoSuchWorkspaceException; +import javax.security.auth.login.LoginException; + +/** + * Oak content repository. The repository may be local or remote, or a cluster + * of any size. These deployment details are all hidden behind this interface. + *

+ * All access to the repository happens through authenticated + * {@link ContentSession} instances acquired through the + * {@link #login(Credentials, String)} method, which is why that is the only + * method of this interface. + *

+ * Starting and stopping ContentRepository instances is the responsibility + * of each particular deployment and not covered by this interface. + * Repository clients should use a deployment-specific mechanism (JNDI, + * OSGi service, etc.) to acquire references to ContentRepository instances. + *

+ * This interface is thread-safe. + */ +public interface ContentRepository { + + /** + * Authenticates a user based on the given credentials or available + * out-of-band information and, if successful, returns a + * {@link ContentSession} instance for accessing repository content + * inside the specified workspace as the authenticated user. + *

+ * TODO: Determine whether ContentSessions should cover a single + * workspace or the entire repository. + *

+ * The exact type of access credentials is undefined, as this method + * simply acts as a generic messenger between clients and pluggable + * login modules that take care of the actual authentication. See + * the documentation of relevant login modules for the kind of access + * credentials they expect. + *

+ * TODO: Instead of the explicit access credentials, should this method + * rather take the arguments to be passed to the relevant + * JAAS {@link javax.security.auth.login.LoginContext} constructor? + *

+ * The client must explicitly {@link ContentSession#close()} the + * returned session once it is no longer used. The recommended access + * pattern is: + *

+     * ContentRepository repository = ...;
+     * ContentSession session = repository.login(...);
+     * try {
+     *     ...; // Use the session
+     * } finally {
+     *     session.close();
+     * }
+     * 
+ * + * @param credentials access credentials, or {@code null} + * @param workspaceName + * @return authenticated repository session + * @throws LoginException if authentication failed + * @throws NoSuchWorkspaceException + */ + @Nonnull + ContentSession login(Credentials credentials, String workspaceName) + throws LoginException, NoSuchWorkspaceException; + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/api/ContentSession.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/ContentSession.java new file mode 100644 index 00000000000..b100c61c341 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/ContentSession.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.api; + +import java.io.Closeable; +import javax.annotation.Nonnull; + +/** + * Authentication session for accessing (TODO: a workspace inside) a content + * repository. + *

+ * - retrieving information from persistent layer (MK) that are accessible to + * a given JCR session + * + * - validate information being written back to the persistent layer. this includes + * permission evaluation, node type and name constraints etc. + * + * - Provide access to authentication and authorization related information + * + * - The implementation of this and all related interfaces are intended to only + * hold the state of the persistent layer at a given revision without any + * session-related state modifications. + *

+ * TODO: describe how this interface is intended to handle validation: + * nt, names, ac, constraints... + *

+ * This interface is thread-safe. + */ +public interface ContentSession extends Closeable { + + /** + * This methods provides access to information related to authentication + * and authorization of this content session. Multiple calls to this method + * may return different instances which are guaranteed to be equal wrt. + * to {@link Object#equals(Object)}. + * + * @return immutable {@link AuthInfo} instance + */ + @Nonnull + AuthInfo getAuthInfo(); + + /** + * TODO clarify workspace handling + * The name of the workspace this {@code ContentSession} instance has + * been created for. If no workspace name has been specified during + * repository login this method will return the name of the default + * workspace. + * + * @return name of the workspace this instance has been created for or + * {@code null} if this content session is repository bound. + */ + String getWorkspaceName(); + + /** + * The current head root as seen by this content session. Use + * {@link Root#commit()} to atomically apply the changes made + * in that subtree the underlying Microkernel. + *

+ * The root instance gives you a stable view of the tree at the time the + * root is acquired. In certain setups (i.e. clusters) changes committed + * through other sessions might not be immediately reflected through this + * call.

+ * Please note this method is possibly expensive because it internally reads + * from the backend to detect if there were any changes (from any session). + * + * @return the current head root + */ + @Nonnull + Root getLatestRoot(); +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/api/PropertyState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/PropertyState.java new file mode 100644 index 00000000000..1ca0305ada7 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/PropertyState.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.api; + +import javax.annotation.Nonnull; + +/** + * Immutable property state. A property consists of a name and a value. + * A value is either an atom or an array of atoms. + * + *

Equality and hash codes

+ *

+ * Two property states are considered equal if and only if their names and + * values match. The {@link Object#equals(Object)} method needs to + * be implemented so that it complies with this definition. And while + * property states are not meant for use as hash keys, the + * {@link Object#hashCode()} method should still be implemented according + * to this equality contract. + */ +public interface PropertyState { + + /** + * @return the name of this property state + */ + @Nonnull + String getName(); + + /** + * Determine whether the value is an array of atoms + * @return {@code true} if and only if the value is an array of atoms. + */ + boolean isArray(); + + /** + * Determine the type of this property + * @return the type of this property + */ + Type getType(); + + /** + * Value of this property. + * The type of the return value is determined by the target {@code type} + * argument. If {@code type.isArray()} is true, this method returns an + * {@code Iterable} of the {@link Type#getBaseType() base type} of + * {@code type} containing all values of this property. + * If the target type is not the same as the type of this property an attempt + * is made to convert the value to the target type. If the conversion fails an + * exception is thrown. The actual conversions which take place are those defined + * in the {@link org.apache.jackrabbit.oak.plugins.value.Conversions} class. + * @param type target type + * @param + * @return the value of this property + * @throws IllegalStateException if {@code type.isArray() == false} and + * {@code this.isArray() == true}. In other words, when trying to convert + * from an array to an atom. + * @throws IllegalArgumentException if {@code type} refers to an unknown type. + * @throws NumberFormatException if conversion to a number failed. + * @throws UnsupportedOperationException if conversion to boolean failed. + */ + @Nonnull + T getValue(Type type); + + /** + * Value at the given {@code index}. + * The type of the return value is determined by the target {@code type} + * argument. + * If the target type is not the same as the type of this property an attempt + * is made to convert the value to the target type. If the conversion fails an + * exception is thrown. The actual conversions which take place are those defined + * in the {@link org.apache.jackrabbit.oak.plugins.value.Conversions} class. + * @param type target type + * @param index + * @param + * @return the value of this property at the given {@code index} + * @throws IndexOutOfBoundsException if {@code index} is less than {@code 0} or + * greater or equals {@code count()}. + * @throws IllegalArgumentException if {@code type} refers to an unknown type or if + * {@code type.isArray()} is true. + */ + @Nonnull + T getValue(Type type, int index); + + /** + * The size of the value of this property. + * @return size of the value of this property + * @throws IllegalStateException if the value is an array + */ + long size(); + + /** + * The size of the value at the given {@code index}. + * @param index + * @return size of the value at the given {@code index}. + * @throws IndexOutOfBoundsException if {@code index} is less than {@code 0} or + * greater or equals {@code count()}. + */ + long size(int index); + + /** + * The number of values of this property. {@code 1} for atoms. + * @return number of values + */ + int count(); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/api/PropertyValue.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/PropertyValue.java new file mode 100644 index 00000000000..b0e34775de0 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/PropertyValue.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.api; + +import javax.annotation.Nonnull; + +/** + * Immutable property value. + * A value is either an atom or an array of atoms. + * + */ +public interface PropertyValue extends Comparable { + + /** + * Determine whether the value is an array of atoms + * @return {@code true} if and only if the value is an array of atoms. + */ + boolean isArray(); + + /** + * Determine the type of this value + * @return the type of this value + */ + Type getType(); + + /** + * Value of this object. + * The type of the return value is determined by the target {@code type} + * argument. If {@code type.isArray()} is true, this method returns an + * {@code Iterable} of the {@link Type#getBaseType() base type} of + * {@code type} containing all values of this property. + * If the target type is not the same as the type of this property an attempt + * is made to convert the value to the target type. If the conversion fails an + * exception is thrown. + * @param type target type + * @param + * @return the value of this property + * @throws IllegalStateException if {@code type.isArray() == false} and + * {@code this.isArray() == true}. In other words, when trying to convert + * from an array to an atom. + * @throws IllegalArgumentException if {@code type} refers to an unknown type. + * @throws NumberFormatException if conversion to a number failed. + * @throws UnsupportedOperationException if conversion to boolean failed. + */ + @Nonnull + T getValue(Type type); + + /** + * Value at the given {@code index}. + * The type of the return value is determined by the target {@code type} + * argument. + * If the target type is not the same as the type of this property an attempt + * is made to convert the value to the target type. If the conversion fails an + * exception is thrown. + * @param type target type + * @param index + * @param + * @return the value of this object at the given {@code index} + * @throws IndexOutOfBoundsException if {@code index} is less than {@code 0} or + * greater or equals {@code count()}. + * @throws IllegalArgumentException if {@code type} refers to an unknown type or if + * {@code type.isArray()} is true. + */ + @Nonnull + T getValue(Type type, int index); + + /** + * The size of the value of this object. + * @return size of the value of this property + * @throws IllegalStateException if the value is an array + */ + long size(); + + /** + * The size of the value at the given {@code index}. + * @param index + * @return size of the value at the given {@code index}. + * @throws IndexOutOfBoundsException if {@code index} is less than {@code 0} or + * greater or equals {@code count()}. + */ + long size(int index); + + /** + * The number of values of this object. {@code 1} for atoms. + * @return number of values + */ + int count(); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Result.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Result.java new file mode 100644 index 00000000000..efa9641cd4b --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Result.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.api; + +/** + * A result from executing a query. + */ +public interface Result { + + /** + * Get the list of column names. + * + * @return the column names + */ + String[] getColumnNames(); + + /** + * Get the list of selector names. + * + * @return the selector names + */ + String[] getSelectorNames(); + + /** + * Get the rows. + * + * @return the rows + */ + Iterable getRows(); + + /** + * Get the number of rows, if known. If the size is not known, -1 is + * returned. + * + * @return the size or -1 if unknown + */ + long getSize(); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/Commit.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/ResultRow.java similarity index 74% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/model/Commit.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/api/ResultRow.java index 82501a0450b..9e9ba939b13 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/Commit.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/ResultRow.java @@ -14,22 +14,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.model; +package org.apache.jackrabbit.oak.api; -import org.apache.jackrabbit.mk.store.Binding; /** - * + * A query result row. */ -public interface Commit { - - Id getRootNodeId(); +public interface ResultRow { + + String getPath(); - public Id getParentId(); + String getPath(String selectorName); - public long getCommitTS(); + PropertyValue getValue(String columnName); - public String getMsg(); + PropertyValue[] getValues(); - void serialize(Binding binding) throws Exception; } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Root.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Root.java new file mode 100644 index 00000000000..0bd9aa48c46 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Root.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.api; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +/** + * The root of a {@link Tree}. + *

+ * The data returned by this class filtered for the access rights that are set + * in the {@link ContentSession} that created this object. + *

+ * All root instances created by a content session become invalid after the + * content session is closed. Any method called on an invalid root instance + * will throw an {@code InvalidStateException}. + */ +public interface Root { + + /** + * Move the child located at {@code sourcePath} to a child at {@code destPath}. + * Both paths must be absolute and resolve to a child located beneath this + * root.
+ * + * This method does nothing and returns {@code false} if + *

    + *
  • the tree at {@code sourcePath} does not exist or is not accessible,
  • + *
  • the parent of the tree at {@code destinationPath} does not exist or is not accessible,
  • + *
  • a tree already exists at {@code destinationPath}.
  • + *
+ * If a tree at {@code destinationPath} exists but is not accessible to the + * editing content session this method succeeds but a subsequent + * {@link #commit()} will detect the violation and fail. + * + * @param sourcePath The source path + * @param destPath The destination path + * @return {@code true} on success, {@code false} otherwise. + */ + boolean move(String sourcePath, String destPath); + + /** + * Copy the child located at {@code sourcePath} to a child at {@code destPath}. + * Both paths must be absolute and resolve to a child located in this root.
+ * + * This method does nothing an returns {@code false} if + *
    + *
  • The tree at {@code sourcePath} does exist or is not accessible,
  • + *
  • the parent of the tree at {@code destinationPath} does not exist or is not accessible,
  • + *
  • a tree already exists at {@code destinationPath}.
  • + *
+ * If a tree at {@code destinationPath} exists but is not accessible to the + * editing content session this method succeeds but a subsequent + * {@link #commit()} will detect the violation and fail. + * + * @param sourcePath source path + * @param destPath destination path + * @return {@code true} on success, {@code false} otherwise. + */ + boolean copy(String sourcePath, String destPath); + + /** + * Retrieve the {@code Tree} at the given absolute {@code path}. The path + * must resolve to a tree in this root. + * + * @param path absolute path to the tree + * @return tree at the given path or {@code null} if no such tree exists or + * if the tree at {@code path} is not accessible. + */ + @CheckForNull + Tree getTree(String path); + + /** + * Get a tree location for a given absolute {@code path} + * + * @param path absolute path to the location + * @return the tree location for {@code path} + */ + @Nonnull + TreeLocation getLocation(String path); + + /** + * Rebase this root instance to the latest revision. After a call to this method, + * all trees obtained through {@link #getTree(String)} become invalid and fresh + * instances must be obtained. + */ + void rebase(); + + /** + * Reverts all changes made to this root and refreshed to the latest trunk. + * After a call to this method, all trees obtained through {@link #getTree(String)} + * become invalid and fresh instances must be obtained. + */ + void refresh(); + + /** + * Atomically apply all changes made to the tree beneath this root to the + * underlying store and refreshes this root. After a call to this method, + * all trees obtained through {@link #getTree(String)} become invalid and fresh + * instances must be obtained. + * + * @throws CommitFailedException + */ + void commit() throws CommitFailedException; + + /** + * Determine whether there are changes on this tree + * @return {@code true} iff this tree was modified + */ + boolean hasPendingChanges(); + + /** + * Get the query engine. + * + * @return the query engine + */ + @Nonnull + SessionQueryEngine getQueryEngine(); + + /** + * Returns the blob factory (TODO: review if that really belongs to the OAK-API. see also todos on BlobFactory) + * + * @return the blob factory. + */ + @Nonnull + BlobFactory getBlobFactory(); +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/api/SessionQueryEngine.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/SessionQueryEngine.java new file mode 100644 index 00000000000..2f029a13ad7 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/SessionQueryEngine.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.api; + +import java.text.ParseException; +import java.util.List; +import java.util.Map; + +import org.apache.jackrabbit.oak.namepath.NamePathMapper; + +/** + * The query engine allows to parse and execute queries. + *

+ * What query languages are supported depends on the registered query parsers. + */ +public interface SessionQueryEngine { + + /** + * Get the list of supported query languages. + * + * @return the supported query languages + */ + List getSupportedQueryLanguages(); + + /** + * Parse the query (check if it's valid) and get the list of bind variable names. + * + * @param statement + * @param language + * @return the list of bind variable names + * @throws ParseException + */ + List getBindVariableNames(String statement, String language) throws ParseException; + + /** + * Execute a query and get the result. + * + * @param statement the query statement + * @param language the language + * @param limit the maximum result set size + * @param offset the number of rows to skip + * @param bindings the bind variable value bindings + * @param namePathMapper the name and path mapper to use + * @return the result + * @throws ParseException if the statement could not be parsed + * @throws IllegalArgumentException if there was an error executing the query + */ + Result executeQuery(String statement, String language, + long limit, long offset, Map bindings, + NamePathMapper namePathMapper) throws ParseException; + + // TODO pass namespace mapping + // TODO pass node type information (select * from [xyz] is supposed to return at least the mandatory columns for xyz) + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Tree.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Tree.java new file mode 100644 index 00000000000..acbad987d61 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Tree.java @@ -0,0 +1,277 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.api; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +/** + * A tree instance represents a snapshot of the {@code ContentRepository} + * tree at the time the instance was acquired. Tree instances may + * become invalid over time due to garbage collection of old content, at + * which point an outdated snapshot will start throwing + * {@code IllegalStateException}s to indicate that the snapshot is no + * longer available. + *

+ * The children of a {@code Tree} are generally unordered. That is, the + * sequence of the children returned by {@link #getChildren()} may change over + * time as this Tree is modified either directly or through some other session. + * Calling {@link #orderBefore(String)} will persist the current order and + * maintain the order as new children are added or removed. In this case a new + * child will be inserted after the last child as seen by {@link #getChildren()}. + *

+ * A tree instance belongs to the client and its state is only modified + * in response to method calls made by the client. The various accessors + * on this interface mirror these of the underlying {@code NodeState} + * interface. However, since instances of this class are mutable return + * values may change between invocations. + *

+ * Tree instances are not thread-safe for write access, so writing clients + * need to ensure that they are not accessed concurrently from multiple + * threads. Instances are however thread-safe for read access, so + * implementations need to ensure that all reading clients see a + * coherent state. + *

+ * The data returned by this class and intermediary objects such as + * {@link PropertyState} is filtered for the access rights that are set in the + * {@link ContentSession} that created the {@link Root} of this object. + *

+ * All tree instances created in the context of a content session become invalid + * after the content session is closed. Any method called on an invalid tree instance + * will throw an {@code InvalidStateException}. + */ +public interface Tree { + + /** + * Status of an item in a {@code Tree} + */ + enum Status { + /** + * Item is persisted + */ + EXISTING, + + /** + * Item is new + */ + NEW, + + /** + * Item is modified: has added or removed children or added, removed or modified + * properties. + */ + MODIFIED, + + /** + * Item is removed + */ + REMOVED + } + + /** + * @return the name of this {@code Tree} instance. + */ + @Nonnull + String getName(); + + /** + * @return {@code true} iff this is the root + */ + boolean isRoot(); + + /** + * @return the absolute path of this {@code Tree} instance from its {@link Root}. + */ + @Nonnull + String getPath(); + + /** + * Get the {@code Status} of this tree instance. + * + * @return The status of this tree instance. + */ + @Nonnull + Status getStatus(); + + /** + * @return the current location + */ + @Nonnull + TreeLocation getLocation(); + + /** + * @return the parent of this {@code Tree} instance. This method returns + * {@code null} if the parent is not accessible or if no parent exists (root + * node). + */ + @CheckForNull + Tree getParent(); + + /** + * Get a property state + * + * @param name The name of the property state. + * @return the property state with the given {@code name} or {@code null} + * if no such property state exists or the property is not accessible. + */ + @CheckForNull + PropertyState getProperty(String name); + + /** + * Get the {@code Status} of a property state or {@code null}. + * + * @param name The name of the property state. + * @return The status of the property state with the given {@code name} + * or {@code null} in no such property state exists or if the name refers + * to a property that is not accessible. + */ + @CheckForNull + Status getPropertyStatus(String name); + + /** + * Determine if a property state exists and is accessible. + * + * @param name The name of the property state + * @return {@code true} if and only if a property with the given {@code name} + * exists and is accessible. + */ + boolean hasProperty(String name); + + /** + * Determine the number of properties accessible to the current content session. + * + * @return The number of accessible properties. + */ + long getPropertyCount(); + + /** + * All accessible property states. The returned {@code Iterable} has snapshot + * semantics. That is, it reflect the state of this {@code Tree} instance at + * the time of the call. Later changes to this instance are no visible to + * iterators obtained from the returned iterable. + * + * @return An {@code Iterable} for all accessible property states. + */ + @Nonnull + Iterable getProperties(); + + /** + * Get a child of this {@code Tree} instance. + * + * @param name The name of the child to retrieve. + * @return The child with the given {@code name} or {@code null} if no such + * child exists or the child is not accessible. + */ + @CheckForNull + Tree getChild(String name); + + /** + * Determine if a child of this {@code Tree} instance exists. If no child + * exists or an existing child isn't accessible this method returns {@code false}. + * + * @param name The name of the child + * @return {@code true} if and only if a child with the given {@code name} + * exists and is accessible for the current content session. + */ + boolean hasChild(String name); + + /** + * Determine the number of children of this {@code Tree} instance taking + * access restrictions into account. + * + * @return The number of accessible children. + */ + long getChildrenCount(); + + /** + * All accessible children of this {@code Tree} instance. The returned + * {@code Iterable} has snapshot semantics. That is, it reflect the state of + * this {@code Tree} instance at the time of the call. Later changes to this + * instance are not visible to iterators obtained from the returned iterable. + * + * @return An {@code Iterable} for all accessible children + */ + @Nonnull + Iterable getChildren(); + + /** + * Remove this tree instance. This operation never succeeds for the root tree. + * + * @return {@code true} if the node was removed; {@code false} otherwise. + */ + boolean remove(); + + /** + * Add a child with the given {@code name}. Does nothing if such a child + * already exists. + * + * @param name name of the child + * @return the {@code Tree} instance of the child with the given {@code name}. + */ + @Nonnull + Tree addChild(String name); + + /** + * Orders this {@code Tree} before the sibling tree with the given + * {@code name}. Calling this method for the first time on this + * {@code Tree} or any of its siblings will persist the current order + * of siblings and maintain it from this point on. + * + * @param name the name of the sibling node where this tree is ordered + * before. This tree will become the last sibling if + * {@code name} is {@code null}. + * @return {@code false} if there is no sibling with the given + * {@code name} and no reordering was performed; + * {@code true} otherwise. + */ + boolean orderBefore(String name); + + /** + * Set a property state + * @param property The property state to set + */ + void setProperty(PropertyState property); + + /** + * Set a property state + * @param name The name of this property + * @param value The value of this property + * @param The type of this property. Must be one of {@code String, Blob, byte[], Long, Integer, Double, Boolean, BigDecimal} + * @throws IllegalArgumentException if {@code T} is not one of the above types. + */ + void setProperty(String name, T value); + + /** + * Set a property state + * @param name The name of this property + * @param value The value of this property + * @param type The type of this property. + * @param The type of this property. + */ + void setProperty(String name, T value, Type type); + + /** + * Remove the property with the given name. This method has no effect if a + * property of the given {@code name} does not exist. + * + * @param name The name of the property + */ + void removeProperty(String name); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/api/TreeLocation.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/TreeLocation.java new file mode 100644 index 00000000000..64935b7a22a --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/TreeLocation.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.api; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.Tree.Status; + +/** + * A {@code TreeLocation} denotes a location inside a tree. + * It can either refer to a inner node (that is a {@link org.apache.jackrabbit.oak.api.Tree}) + * or to a leaf (that is a {@link org.apache.jackrabbit.oak.api.PropertyState}). + * {@code TreeLocation} instances provide methods for navigating trees. {@code TreeLocation} + * instances are immutable and navigating a tree always results in new {@code TreeLocation} + * instances. Navigation never fails. Errors are deferred until the underlying item itself is + * accessed. That is, if a {@code TreeLocation} points to an item which does not exist or + * is unavailable otherwise (i.e. due to access control restrictions) accessing the tree + * will return {@code null} at this point. + */ +public interface TreeLocation { + + /** + * Navigate to the parent + * @return a {@code TreeLocation} for the parent of this location. + */ + @Nonnull + TreeLocation getParent(); + + /** + * Navigate to a child through a relative path. A relative path consists of a + * possibly empty lists of names separated by forward slashes. + * @param relPath relative path to the child + * @return a {@code TreeLocation} for a child with the given {@code name}. + */ + @Nonnull + TreeLocation getChild(String relPath); + + /** + * Get the underlying {@link org.apache.jackrabbit.oak.api.Tree} for this {@code TreeLocation}. + * @return underlying {@code Tree} instance or {@code null} if not available. + */ + @CheckForNull + Tree getTree(); + + /** + * Get the underlying {@link org.apache.jackrabbit.oak.api.PropertyState} for this {@code TreeLocation}. + * @return underlying {@code PropertyState} instance or {@code null} if not available. + */ + @CheckForNull + PropertyState getProperty(); + + /** + * {@link org.apache.jackrabbit.oak.api.Tree.Status} of the underlying item or {@code null} if no + * such item exists. + * @return + */ + @CheckForNull + Status getStatus(); + + /** + * The path of the underlying item or {@code null} if no such item exists. + * @return path + */ + @CheckForNull + String getPath(); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Type.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Type.java new file mode 100644 index 00000000000..0d5c639e05c --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Type.java @@ -0,0 +1,199 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.api; + +import java.math.BigDecimal; + +import javax.jcr.PropertyType; + +import com.google.common.base.Objects; + +import static com.google.common.base.Preconditions.checkState; + +/** + * Instances of this class map Java types to {@link PropertyType property types}. + * Passing an instance of this class to {@link PropertyState#getValue(Type)} determines + * the return type of that method. + * @param + */ +public final class Type { + + /** Map {@code String} to {@link PropertyType#STRING} */ + public static final Type STRING = create(PropertyType.STRING, false); + + /** Map {@code Blob} to {@link PropertyType#BINARY} */ + public static final Type BINARY = create(PropertyType.BINARY, false); + + /** Map {@code Long} to {@link PropertyType#LONG} */ + public static final Type LONG = create(PropertyType.LONG, false); + + /** Map {@code Double} to {@link PropertyType#DOUBLE} */ + public static final Type DOUBLE = create(PropertyType.DOUBLE, false); + + /** Map {@code String} to {@link PropertyType#DATE} */ + public static final Type DATE = create(PropertyType.DATE, false); + + /** Map {@code Boolean} to {@link PropertyType#BOOLEAN} */ + public static final Type BOOLEAN = create(PropertyType.BOOLEAN, false); + + /** Map {@code String} to {@link PropertyType#STRING} */ + public static final Type NAME = create(PropertyType.NAME, false); + + /** Map {@code String} to {@link PropertyType#PATH} */ + public static final Type PATH = create(PropertyType.PATH, false); + + /** Map {@code String} to {@link PropertyType#REFERENCE} */ + public static final Type REFERENCE = create(PropertyType.REFERENCE, false); + + /** Map {@code String} to {@link PropertyType#WEAKREFERENCE} */ + public static final Type WEAKREFERENCE = create(PropertyType.WEAKREFERENCE, false); + + /** Map {@code String} to {@link PropertyType#URI} */ + public static final Type URI = create(PropertyType.URI, false); + + /** Map {@code BigDecimal} to {@link PropertyType#DECIMAL} */ + public static final Type DECIMAL = create(PropertyType.DECIMAL, false); + + /** Map {@code Iterable} to array of {@link PropertyType#STRING} */ + public static final Type> STRINGS = create(PropertyType.STRING, true); + + /** Map {@code Iterable} to array of {@link PropertyType#BINARY} */ + public static final Type> BINARIES = create(PropertyType.BINARY, true); + + /** Map {@code Iterable} to array of {@link PropertyType#LONG} */ + public static final Type> LONGS = create(PropertyType.LONG, true); + + /** Map {@code Iterable} to array of {@link PropertyType#DOUBLE} */ + public static final Type> DOUBLES = create(PropertyType.DOUBLE, true); + + /** Map {@code Iterable} to array of {@link PropertyType#DATE} */ + public static final Type> DATES = create(PropertyType.DATE, true); + + /** Map {@code Iterable} to array of {@link PropertyType#BOOLEAN} */ + public static final Type> BOOLEANS = create(PropertyType.BOOLEAN, true); + + /** Map {@code Iterable} to array of {@link PropertyType#NAME} */ + public static final Type> NAMES = create(PropertyType.NAME, true); + + /** Map {@code Iterable} to array of {@link PropertyType#PATH} */ + public static final Type> PATHS = create(PropertyType.PATH, true); + + /** Map {@code Iterable} to array of {@link PropertyType#REFERENCE} */ + public static final Type> REFERENCES = create(PropertyType.REFERENCE, true); + + /** Map {@code Iterable} to array of {@link PropertyType#WEAKREFERENCE} */ + public static final Type> WEAKREFERENCES = create(PropertyType.WEAKREFERENCE, true); + + /** Map {@code Iterable} to array of {@link PropertyType#URI} */ + public static final Type> URIS = create(PropertyType.URI, true); + + /** Map {@code Iterable} to array of {@link PropertyType#DECIMAL} */ + public static final Type> DECIMALS = create(PropertyType.DECIMAL, true); + + private final int tag; + private final boolean array; + + private Type(int tag, boolean array){ + this.tag = tag; + this.array = array; + } + + private static Type create(int tag, boolean array) { + return new Type(tag, array); + } + + /** + * Corresponding type tag as defined in {@link PropertyType}. + * @return type tag + */ + public int tag() { + return tag; + } + + /** + * Determine whether this is an array type + * @return {@code true} if and only if this is an array type + */ + public boolean isArray() { + return array; + } + + /** + * Corresponding {@code Type} for a given type tag and array flag. + * @param tag type tag as defined in {@link PropertyType}. + * @param array whether this is an array or not + * @return {@code Type} instance + * @throws IllegalArgumentException if tag is not valid as per definition in {@link PropertyType}. + */ + public static Type fromTag(int tag, boolean array) { + switch (tag) { + case PropertyType.STRING: return array ? STRINGS : STRING; + case PropertyType.BINARY: return array ? BINARIES : BINARY; + case PropertyType.LONG: return array ? LONGS : LONG; + case PropertyType.DOUBLE: return array ? DOUBLES : DOUBLE; + case PropertyType.DATE: return array ? DATES: DATE; + case PropertyType.BOOLEAN: return array ? BOOLEANS: BOOLEAN; + case PropertyType.NAME: return array ? NAMES : NAME; + case PropertyType.PATH: return array ? PATHS: PATH; + case PropertyType.REFERENCE: return array ? REFERENCES : REFERENCE; + case PropertyType.WEAKREFERENCE: return array ? WEAKREFERENCES : WEAKREFERENCE; + case PropertyType.URI: return array ? URIS: URI; + case PropertyType.DECIMAL: return array ? DECIMALS : DECIMAL; + default: throw new IllegalArgumentException("Invalid type tag: " + tag); + } + } + + /** + * Determine the base type of array types + * @return base type + * @throws IllegalStateException if {@code isArray} is false. + */ + public Type getBaseType() { + checkState(isArray(), "Not an array"); + return fromTag(tag, false); + } + + /** + * Determine the array type which has this type as base type + * @return array type with this type as base type + * @throws IllegalStateException if {@code isArray} is true. + */ + public Type getArrayType() { + checkState(!isArray(), "Not a simply type"); + return fromTag(tag, true); + } + + @Override + public String toString() { + return isArray() + ? "[]" + PropertyType.nameFromValue(getBaseType().tag) + : PropertyType.nameFromValue(tag); + } + + @Override + public int hashCode() { + return Objects.hashCode(tag, array); + } + + @Override + public boolean equals(Object other) { + return this == other; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/Constants.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/package-info.java similarity index 79% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/Constants.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/api/package-info.java index 38b83a0f14c..7f688f4d91d 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/Constants.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/package-info.java @@ -14,14 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk; /** - * Constants used in this project. + * Oak repository API */ -public class Constants { +@Version("0.1") +@Export(optional = "provide:=true") +package org.apache.jackrabbit.oak.api; - public static final boolean NODE_NAME_AS_PROPERTY = false; - public static final boolean JSON_NEWLINES = false; +import aQute.bnd.annotation.Export; +import aQute.bnd.annotation.Version; -} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/core/ContentRepositoryImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/core/ContentRepositoryImpl.java new file mode 100644 index 00000000000..d13d8503189 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/core/ContentRepositoryImpl.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.core; + +import javax.annotation.Nonnull; +import javax.jcr.Credentials; +import javax.jcr.NoSuchWorkspaceException; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.spi.commit.ConflictHandler; +import org.apache.jackrabbit.oak.spi.query.CompositeQueryIndexProvider; +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +import org.apache.jackrabbit.oak.spi.security.authentication.LoginContext; +import org.apache.jackrabbit.oak.spi.security.authentication.LoginContextProvider; +import org.apache.jackrabbit.oak.spi.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.oak.spi.state.NodeStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link MicroKernel}-based implementation of + * the {@link ContentRepository} interface. + */ +public class ContentRepositoryImpl implements ContentRepository { + + /** Logger instance */ + private static final Logger LOG = LoggerFactory.getLogger(ContentRepositoryImpl.class); + + // TODO: retrieve default wsp-name from configuration + private static final String DEFAULT_WORKSPACE_NAME = "default"; + + private final SecurityProvider securityProvider; + private final QueryIndexProvider indexProvider; + private final NodeStore nodeStore; + private final ConflictHandler conflictHandler; + + /** + * Creates an content repository instance based on the given, already + * initialized components. + * + * @param nodeStore the node store this repository is based upon. + * @param indexProvider index provider + * @param securityProvider The configured security provider or {@code null} if + * default implementations should be used. + */ + public ContentRepositoryImpl(NodeStore nodeStore, + ConflictHandler conflictHandler, + QueryIndexProvider indexProvider, + SecurityProvider securityProvider) { + this.nodeStore = nodeStore; + this.conflictHandler = conflictHandler; + this.indexProvider = indexProvider != null ? indexProvider : new CompositeQueryIndexProvider(); + this.securityProvider = securityProvider; + } + + @Nonnull + @Override + public ContentSession login(Credentials credentials, String workspaceName) + throws LoginException, NoSuchWorkspaceException { + if (workspaceName == null) { + workspaceName = DEFAULT_WORKSPACE_NAME; + } + + // TODO: support multiple workspaces. See OAK-118 + if (!DEFAULT_WORKSPACE_NAME.equals(workspaceName)) { + throw new NoSuchWorkspaceException(workspaceName); + } + + LoginContextProvider lcProvider = securityProvider.getLoginContextProvider(nodeStore, indexProvider); + LoginContext loginContext = lcProvider.getLoginContext(credentials, workspaceName); + loginContext.login(); + + AccessControlProvider acProvider = securityProvider.getAccessControlProvider(); + return new ContentSessionImpl(loginContext, acProvider, workspaceName, + nodeStore, conflictHandler, indexProvider); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/core/ContentSessionImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/core/ContentSessionImpl.java new file mode 100644 index 00000000000..c8a98ae3f88 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/core/ContentSessionImpl.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.core; + +import java.io.IOException; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.oak.api.AuthInfo; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.spi.commit.ConflictHandler; +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.apache.jackrabbit.oak.spi.security.authentication.LoginContext; +import org.apache.jackrabbit.oak.spi.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.oak.spi.state.NodeStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkState; + +/** + * {@code MicroKernel}-based implementation of the {@link ContentSession} interface. + */ +class ContentSessionImpl implements ContentSession { + + private static final Logger log = LoggerFactory.getLogger(ContentSessionImpl.class); + + private final LoginContext loginContext; + private final AccessControlProvider accProvider; + private final String workspaceName; + private final NodeStore store; + private final ConflictHandler conflictHandler; + private final QueryIndexProvider indexProvider; + + private volatile boolean live = true; + + public ContentSessionImpl(LoginContext loginContext, + AccessControlProvider accProvider, String workspaceName, + NodeStore store, ConflictHandler conflictHandler, + QueryIndexProvider indexProvider) { + this.loginContext = loginContext; + this.accProvider = accProvider; + this.workspaceName = workspaceName; + this.store = store; + this.conflictHandler = conflictHandler; + this.indexProvider = indexProvider; + } + + private void checkLive() { + checkState(live, "This session has been closed"); + } + + //-----------------------------------------------------< ContentSession >--- + @Nonnull + @Override + public AuthInfo getAuthInfo() { + checkLive(); + Set infoSet = loginContext.getSubject().getPublicCredentials(AuthInfo.class); + if (infoSet.isEmpty()) { + return AuthInfo.EMPTY; + } else { + return infoSet.iterator().next(); + } + } + + @Override + public String getWorkspaceName() { + return workspaceName; + } + + @Nonnull + @Override + public Root getLatestRoot() { + checkLive(); + RootImpl root = new RootImpl(store, workspaceName, loginContext.getSubject(), accProvider, indexProvider) { + @Override + protected void checkLive() { + ContentSessionImpl.this.checkLive(); + } + }; + if (conflictHandler != null) { + root.setConflictHandler(conflictHandler); + } + return root; + } + + //-----------------------------------------------------------< Closable >--- + @Override + public synchronized void close() throws IOException { + try { + loginContext.logout(); + live = false; + } catch (LoginException e) { + log.error("Error during logout.", e); + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/core/MergingNodeStateDiff.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/core/MergingNodeStateDiff.java new file mode 100644 index 00000000000..4603cede750 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/core/MergingNodeStateDiff.java @@ -0,0 +1,309 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.core; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.commit.ConflictHandlerWrapper; +import org.apache.jackrabbit.oak.plugins.memory.MemoryPropertyBuilder; +import org.apache.jackrabbit.oak.spi.commit.ConflictHandler; +import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; +import org.apache.jackrabbit.oak.spi.state.PropertyBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static org.apache.jackrabbit.oak.spi.commit.ConflictHandler.Resolution.MERGED; +import static org.apache.jackrabbit.oak.spi.commit.ConflictHandler.Resolution.OURS; + +/** + * MergingNodeStateDiff... TODO + */ +class MergingNodeStateDiff implements NodeStateDiff { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(MergingNodeStateDiff.class); + + private final NodeBuilder target; + private final ConflictHandler conflictHandler; + + private MergingNodeStateDiff(NodeBuilder target, ConflictHandler conflictHandler) { + this.target = target; + this.conflictHandler = conflictHandler; + } + + static void merge(NodeState fromState, NodeState toState, final NodeBuilder target, + final ConflictHandler conflictHandler) { + toState.compareAgainstBaseState(fromState, new MergingNodeStateDiff( + checkNotNull(target), new ChildOrderConflictHandler(conflictHandler))); + } + + //------------------------------------------------------< NodeStateDiff >--- + @Override + public void propertyAdded(PropertyState after) { + ConflictHandler.Resolution resolution; + PropertyState p = target.getProperty(after.getName()); + + if (p == null) { + resolution = OURS; + } + else { + resolution = conflictHandler.addExistingProperty(target, after, p); + } + + switch (resolution) { + case OURS: + target.setProperty(after); + break; + case THEIRS: + case MERGED: + break; + } + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) { + checkArgument(before.getName().equals(after.getName()), + "before and after must have the same name"); + + ConflictHandler.Resolution resolution; + PropertyState p = target.getProperty(after.getName()); + + if (p == null) { + resolution = conflictHandler.changeDeletedProperty(target, after); + } + else if (before.equals(p)) { + resolution = OURS; + } + else { + resolution = conflictHandler.changeChangedProperty(target, after, p); + } + + switch (resolution) { + case OURS: + target.setProperty(after); + break; + case THEIRS: + case MERGED: + break; + } + } + + @Override + public void propertyDeleted(PropertyState before) { + ConflictHandler.Resolution resolution; + PropertyState p = target.getProperty(before.getName()); + + if (before.equals(p)) { + resolution = OURS; + } + else if (p == null) { + resolution = conflictHandler.deleteDeletedProperty(target, before); + } + else { + resolution = conflictHandler.deleteChangedProperty(target, p); + } + + switch (resolution) { + case OURS: + target.removeProperty(before.getName()); + break; + case THEIRS: + case MERGED: + break; + } + } + + @Override + public void childNodeAdded(String name, NodeState after) { + ConflictHandler.Resolution resolution; + if (!target.hasChildNode(name)) { + resolution = OURS; + } else { + NodeBuilder n = target.child(name); + resolution = conflictHandler.addExistingNode(target, name, after, n.getNodeState()); + } + + switch (resolution) { + case OURS: + addChild(target, name, after); + break; + case THEIRS: + case MERGED: + break; + } + } + + @Override + public void childNodeChanged(String name, NodeState before, NodeState after) { + ConflictHandler.Resolution resolution; + if (!target.hasChildNode(name)) { + resolution = conflictHandler.changeDeletedNode(target, name, after); + } else { + merge(before, after, target.child(name), conflictHandler); + resolution = MERGED; + } + + switch (resolution) { + case OURS: + addChild(target, name, after); + break; + case THEIRS: + case MERGED: + break; + } + } + + @Override + public void childNodeDeleted(String name, NodeState before) { + ConflictHandler.Resolution resolution; + NodeBuilder n = target.hasChildNode(name) ? target.child(name) : null; + + if (n == null) { + resolution = conflictHandler.deleteDeletedNode(target, name); + } + else if (before.equals(n.getNodeState())) { + resolution = OURS; + } + else { + resolution = conflictHandler.deleteChangedNode(target, name, n.getNodeState()); + } + + switch (resolution) { + case OURS: + if (n != null) { + removeChild(target, name); + } + break; + case THEIRS: + case MERGED: + break; + } + } + + //---------------------------------------------------------------- + + private static void addChild(NodeBuilder target, String name, NodeState state) { + NodeBuilder child = target.child(name); + for (PropertyState property : state.getProperties()) { + child.setProperty(property); + } + PropertyState childOrder = target.getProperty(TreeImpl.OAK_CHILD_ORDER); + if (childOrder != null) { + PropertyBuilder builder = MemoryPropertyBuilder.create( + Type.STRING, childOrder); + builder.addValue(name); + target.setProperty(builder.getPropertyState(true)); + } + for (ChildNodeEntry entry : state.getChildNodeEntries()) { + addChild(child, entry.getName(), entry.getNodeState()); + } + } + + private static void removeChild(NodeBuilder target, String name) { + target.removeNode(name); + PropertyState childOrder = target.getProperty(TreeImpl.OAK_CHILD_ORDER); + if (childOrder != null) { + PropertyBuilder builder = MemoryPropertyBuilder.create( + Type.STRING, childOrder); + builder.removeValue(name); + target.setProperty(builder.getPropertyState(true)); + } + } + + /** + * ChildOrderConflictHandler ignores conflicts on the + * {@link TreeImpl#OAK_CHILD_ORDER} property. All other conflicts are forwarded + * to the wrapped handler. + */ + private static class ChildOrderConflictHandler extends ConflictHandlerWrapper { + + ChildOrderConflictHandler(ConflictHandler delegate) { + super(delegate); + } + + @Override + public Resolution addExistingProperty(NodeBuilder parent, + PropertyState ours, + PropertyState theirs) { + if (isChildOrderProperty(ours)) { + // two sessions concurrently called orderBefore() on a Tree + // that was previously unordered. + return Resolution.THEIRS; + } else { + return handler.addExistingProperty(parent, ours, theirs); + } + } + + @Override + public Resolution changeDeletedProperty(NodeBuilder parent, + PropertyState ours) { + if (isChildOrderProperty(ours)) { + // orderBefore() on trees that were deleted + return Resolution.THEIRS; + } else { + return handler.changeDeletedProperty(parent, ours); + } + } + + @Override + public Resolution changeChangedProperty(NodeBuilder parent, + PropertyState ours, + PropertyState theirs) { + if (isChildOrderProperty(ours)) { + // concurrent orderBefore(), other changes win + return Resolution.THEIRS; + } else { + return handler.changeChangedProperty(parent, ours, theirs); + } + } + + @Override + public Resolution deleteDeletedProperty(NodeBuilder parent, + PropertyState ours) { + if (isChildOrderProperty(ours)) { + // concurrent remove of ordered trees + return Resolution.THEIRS; + } else { + return handler.deleteDeletedProperty(parent, ours); + } + } + + @Override + public Resolution deleteChangedProperty(NodeBuilder parent, + PropertyState theirs) { + if (isChildOrderProperty(theirs)) { + // remove trees that were reordered by another session + return Resolution.THEIRS; + } else { + return handler.deleteChangedProperty(parent, theirs); + } + } + + //----------------------------< internal >---------------------------------- + + private static boolean isChildOrderProperty(PropertyState p) { + return TreeImpl.OAK_CHILD_ORDER.equals(p.getName()); + } + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/core/ReadOnlyTree.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/core/ReadOnlyTree.java new file mode 100644 index 00000000000..cee7cbb2364 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/core/ReadOnlyTree.java @@ -0,0 +1,219 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.core; + +import java.util.Iterator; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.api.TreeLocation; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class ReadOnlyTree implements Tree { + + /** Parent of this tree, {@code null} for the root */ + private final ReadOnlyTree parent; + + /** Name of this tree */ + private final String name; + + /** Underlying node state */ + private final NodeState state; + + public ReadOnlyTree(NodeState root) { + this(null, "", root); + } + + public ReadOnlyTree(ReadOnlyTree parent, String name, NodeState state) { + this.parent = parent; + this.name = checkNotNull(name); + this.state = checkNotNull(state); + checkArgument(!name.isEmpty() || parent == null); + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean isRoot() { + return parent == null; + } + + @Override + public String getPath() { + if (isRoot()) { + // shortcut + return "/"; + } + + StringBuilder sb = new StringBuilder(); + buildPath(sb); + return sb.toString(); + } + + private void buildPath(StringBuilder sb) { + if (!isRoot()) { + parent.buildPath(sb); + sb.append('/').append(name); + } + } + + @Override + public Tree getParent() { + return parent; + } + + @Override + public PropertyState getProperty(String name) { + return state.getProperty(name); + } + + @Override + public Status getPropertyStatus(String name) { + if (hasProperty(name)) { + return Status.EXISTING; + } else { + return null; + } + } + + @Override + public boolean hasProperty(String name) { + return state.getProperty(name) != null; + } + + @Override + public long getPropertyCount() { + return state.getPropertyCount(); + } + + @Override + public Iterable getProperties() { + return state.getProperties(); + } + + @Override + public ReadOnlyTree getChild(String name) { + NodeState child = state.getChildNode(name); + if (child != null) { + return new ReadOnlyTree(this, name, child); + } else { + return null; + } + } + + @Override + public Status getStatus() { + return Status.EXISTING; + } + + @Override + public TreeLocation getLocation() { + // TODO: add implementation + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasChild(String name) { + return state.getChildNode(name) != null; + } + + @Override + public long getChildrenCount() { + return state.getChildNodeCount(); + } + + /** + * This implementation does not respect ordered child nodes, but always + * returns them in some implementation specific order. + * + * TODO: respect orderable children (needed?) + * @return the children. + */ + @Override + public Iterable getChildren() { + return new Iterable() { + @Override + public Iterator iterator() { + final Iterator iterator = + state.getChildNodeEntries().iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + @Override + public Tree next() { + ChildNodeEntry entry = iterator.next(); + return new ReadOnlyTree( + ReadOnlyTree.this, + entry.getName(), entry.getNodeState()); + } + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + + @Override + public Tree addChild(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean orderBefore(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public void setProperty(PropertyState property) { + throw new UnsupportedOperationException(); + } + + @Override + public void setProperty(String name, T value) { + throw new UnsupportedOperationException(); + } + + @Override + public void setProperty(String name, T value, Type type) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeProperty(String name) { + throw new UnsupportedOperationException(); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/core/RootImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/core/RootImpl.java new file mode 100644 index 00000000000..0c531c75ae3 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/core/RootImpl.java @@ -0,0 +1,372 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.core; + +import java.io.IOException; +import java.io.InputStream; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nonnull; +import javax.security.auth.Subject; + +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.api.BlobFactory; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.SessionQueryEngine; +import org.apache.jackrabbit.oak.api.TreeLocation; +import org.apache.jackrabbit.oak.plugins.commit.DefaultConflictHandler; +import org.apache.jackrabbit.oak.query.SessionQueryEngineImpl; +import org.apache.jackrabbit.oak.spi.commit.ConflictHandler; +import org.apache.jackrabbit.oak.spi.observation.ChangeExtractor; +import org.apache.jackrabbit.oak.spi.query.CompositeQueryIndexProvider; +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.apache.jackrabbit.oak.spi.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.oak.spi.security.authorization.CompiledPermissions; +import org.apache.jackrabbit.oak.spi.security.authorization.OpenAccessControlProvider; +import org.apache.jackrabbit.oak.spi.security.principal.SystemPrincipal; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; +import org.apache.jackrabbit.oak.spi.state.NodeStore; +import org.apache.jackrabbit.oak.spi.state.NodeStoreBranch; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static org.apache.jackrabbit.oak.commons.PathUtils.getName; +import static org.apache.jackrabbit.oak.commons.PathUtils.getParentPath; + +public class RootImpl implements Root { + + /** + * Number of {@link #updated} calls for which changes are kept in memory. + */ + private static final int PURGE_LIMIT = 100; + + /** The underlying store to which this root belongs */ + private final NodeStore store; + + private final Subject subject; + + /** + * The access control context provider. + */ + private final AccessControlProvider accProvider; + + /** Current branch this root operates on */ + private NodeStoreBranch branch; + + /** Current root {@code Tree} */ + private TreeImpl rootTree; + + /** + * Number of {@link #updated} occurred so since the lase + * purge. + */ + private int modCount; + + /** + * Listeners which needs to be notified as soon as {@link #purgePendingChanges()} + * is called. Listeners are removed from this list after being called. If further + * notifications are required, they need to explicitly re-register. + * + * The {@link TreeImpl} instances us this mechanism to dispose of its associated + * {@link NodeBuilder} on purge. Keeping a reference on those {@code TreeImpl} + * instances {@code NodeBuilder} (i.e. those which are modified) prevents them + * from being prematurely garbage collected. + */ + private List purgePurgeListeners = new ArrayList(); + + private volatile ConflictHandler conflictHandler = DefaultConflictHandler.OURS; + + private final QueryIndexProvider indexProvider; + + /** + * Purge listener. + * @see #purgePurgeListeners + */ + public interface PurgeListener { + void purged(); + } + + /** + * New instance bases on a given {@link NodeStore} and a workspace + * + * @param store node store + * @param workspaceName name of the workspace + * @param subject the subject. + * @param accProvider the access control context provider. + * @param indexProvider the query index provider. + */ + @SuppressWarnings("UnusedParameters") + public RootImpl(NodeStore store, + String workspaceName, + Subject subject, + AccessControlProvider accProvider, + QueryIndexProvider indexProvider) { + this.store = checkNotNull(store); + this.subject = checkNotNull(subject); + this.accProvider = checkNotNull(accProvider); + this.indexProvider = indexProvider; + refresh(); + } + + // TODO: review if this constructor really makes sense and cannot be replaced. + public RootImpl(NodeStore store) { + this.store = checkNotNull(store); + this.subject = new Subject(true, Collections.singleton(SystemPrincipal.INSTANCE), Collections.emptySet(), Collections.emptySet()); + this.accProvider = new OpenAccessControlProvider(); + this.indexProvider = new CompositeQueryIndexProvider(); + refresh(); + } + + void setConflictHandler(ConflictHandler conflictHandler) { + this.conflictHandler = conflictHandler; + } + + /** + * Called whenever a method on this instance or on any {@code Tree} instance + * obtained from this {@code Root} is called. This default implementation + * does nothing. Sub classes may override this method and throw an exception + * indicating that this {@code Root} instance is not live anymore (e.g. because + * the session has been logged out already). + */ + protected void checkLive() { + + } + + //---------------------------------------------------------------< Root >--- + @Override + public boolean move(String sourcePath, String destPath) { + checkLive(); + TreeImpl source = rootTree.getTree(sourcePath); + if (source == null) { + return false; + } + TreeImpl destParent = rootTree.getTree(getParentPath(destPath)); + if (destParent == null) { + return false; + } + + String destName = getName(destPath); + if (destParent.hasChild(destName)) { + return false; + } + + purgePendingChanges(); + source.moveTo(destParent, destName); + boolean success = branch.move(sourcePath, destPath); + if (success) { + getTree(getParentPath(sourcePath)).updateChildOrder(); + getTree(getParentPath(destPath)).updateChildOrder(); + } + return success; + } + + @Override + public boolean copy(String sourcePath, String destPath) { + checkLive(); + purgePendingChanges(); + boolean success = branch.copy(sourcePath, destPath); + if (success) { + getTree(getParentPath(destPath)).updateChildOrder(); + } + return success; + } + + @Override + public TreeImpl getTree(String path) { + checkLive(); + return rootTree.getTree(path); + } + + @Override + public TreeLocation getLocation(String path) { + checkLive(); + checkArgument(path.startsWith("/")); + return rootTree.getLocation().getChild(path.substring(1)); + } + + @Override + public void rebase() { + checkLive(); + if (!store.getRoot().equals(rootTree.getBaseState())) { + purgePendingChanges(); + NodeState base = getBaseState(); + NodeState head = rootTree.getNodeState(); + refresh(); + MergingNodeStateDiff.merge(base, head, rootTree.getNodeBuilder(), conflictHandler); + } + } + + @Override + public final void refresh() { + checkLive(); + branch = store.branch(); + rootTree = TreeImpl.createRoot(this); + } + + @Override + public void commit() throws CommitFailedException { + checkLive(); + rebase(); + purgePendingChanges(); + CommitFailedException exception = Subject.doAs( + getCombinedSubject(), new PrivilegedAction() { + @Override + public CommitFailedException run() { + try { + branch.merge(); + return null; + } catch (CommitFailedException e) { + return e; + } + } + }); + if (exception != null) { + throw exception; + } + refresh(); + } + + // TODO: find a better solution for passing in additional principals + private Subject getCombinedSubject() { + Subject accSubject = Subject.getSubject(AccessController.getContext()); + if (accSubject == null) { + return subject; + } + else { + Subject combinedSubject = new Subject(false, + subject.getPrincipals(), subject.getPublicCredentials(), subject.getPrivateCredentials()); + combinedSubject.getPrincipals().addAll(accSubject.getPrincipals()); + combinedSubject.getPrivateCredentials().addAll(accSubject.getPrivateCredentials()); + combinedSubject.getPublicCredentials().addAll((accSubject.getPublicCredentials())); + return combinedSubject; + } + } + + @Override + public boolean hasPendingChanges() { + checkLive(); + return !getBaseState().equals(rootTree.getNodeState()); + } + + @Nonnull + public ChangeExtractor getChangeExtractor() { + checkLive(); + return new ChangeExtractor() { + private NodeState baseLine = store.getRoot(); + + @Override + public void getChanges(NodeStateDiff diff) { + NodeState head = store.getRoot(); + head.compareAgainstBaseState(baseLine, diff); + baseLine = head; + } + }; + } + + @Override + public SessionQueryEngine getQueryEngine() { + checkLive(); + return new SessionQueryEngineImpl(indexProvider) { + @Override + protected NodeState getRootNodeState() { + return rootTree.getNodeState(); + } + + @Override + protected Root getRoot() { + return RootImpl.this; + } + }; + } + + @Nonnull + @Override + public BlobFactory getBlobFactory() { + return new BlobFactory() { + @Override + public Blob createBlob(InputStream inputStream) throws IOException { + checkLive(); + return store.createBlob(inputStream); + } + }; + } + + //-----------------------------------------------------------< internal >--- + + /** + * Returns the node state from which the current branch was created. + * @return base node state + */ + @Nonnull + NodeState getBaseState() { + return branch.getBase(); + } + + NodeBuilder createRootBuilder() { + return branch.getRoot().builder(); + } + + /** + * Add a {@code PurgeListener} to this instance. Listeners are automatically + * unregistered after having been called. If further notifications are required, + * they need to explicitly re-register. + * @param purgeListener listener + */ + void addListener(PurgeListener purgeListener) { + purgePurgeListeners.add(purgeListener); + } + + // TODO better way to determine purge limit. See OAK-175 + void updated() { + if (++modCount > PURGE_LIMIT) { + modCount = 0; + purgePendingChanges(); + } + } + + CompiledPermissions getPermissions() { + return accProvider.getAccessControlContext(subject).getPermissions(); + } + + //------------------------------------------------------------< private >--- + + /** + * Purge all pending changes to the underlying {@link NodeStoreBranch}. + * All registered {@link PurgeListener}s are notified. + */ + private void purgePendingChanges() { + branch.setRoot(rootTree.getNodeState()); + notifyListeners(); + } + + private void notifyListeners() { + List purgeListeners = this.purgePurgeListeners; + this.purgePurgeListeners = new ArrayList(); + + for (PurgeListener purgeListener : purgeListeners) { + purgeListener.purged(); + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/core/TreeImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/core/TreeImpl.java new file mode 100644 index 00000000000..d122f47e5f0 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/core/TreeImpl.java @@ -0,0 +1,790 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.core; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Set; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.api.TreeLocation; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.core.RootImpl.PurgeListener; +import org.apache.jackrabbit.oak.plugins.memory.MemoryPropertyBuilder; +import org.apache.jackrabbit.oak.plugins.memory.MultiStringPropertyState; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; +import org.apache.jackrabbit.oak.spi.state.NodeStateUtils; +import org.apache.jackrabbit.oak.spi.state.PropertyBuilder; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static org.apache.jackrabbit.oak.commons.PathUtils.elements; + +public class TreeImpl implements Tree, PurgeListener { + + /** Internal and hidden property that contains the child order */ + static final String OAK_CHILD_ORDER = ":childOrder"; + + /** Underlying {@code Root} of this {@code Tree} instance */ + private final RootImpl root; + + /** Parent of this tree. Null for the root. */ + private TreeImpl parent; + + /** Marker for removed trees */ + private boolean removed; + + /** Name of this tree */ + private String name; + + /** Lazily initialised {@code NodeBuilder} for the underlying node state */ + NodeBuilder nodeBuilder; + + private TreeImpl(RootImpl root, TreeImpl parent, String name) { + this.root = checkNotNull(root); + this.parent = parent; + this.name = checkNotNull(name); + } + + @Nonnull + static TreeImpl createRoot(final RootImpl root) { + return new TreeImpl(root, null, "") { + @Override + protected NodeState getBaseState() { + return root.getBaseState(); + } + + @Override + protected synchronized NodeBuilder getNodeBuilder() { + if (nodeBuilder == null) { + nodeBuilder = root.createRootBuilder(); + root.addListener(this); + } + return nodeBuilder; + } + }; + } + + @Override + public String getName() { + root.checkLive(); + return name; + } + + @Override + public boolean isRoot() { + root.checkLive(); + return parent == null; + } + + @Override + public String getPath() { + root.checkLive(); + if (isRoot()) { + // shortcut + return "/"; + } + + StringBuilder sb = new StringBuilder(); + buildPath(sb); + return sb.toString(); + } + + @Override + public Tree getParent() { + root.checkLive(); + if (parent != null && canRead(parent)) { + return parent; + } else { + return null; + } + } + + @Override + public PropertyState getProperty(String name) { + root.checkLive(); + PropertyState property = internalGetProperty(name); + if (canRead(property)) { + return property; + } else { + return null; + } + } + + @Override + public Status getPropertyStatus(String name) { + // TODO: see OAK-212 + root.checkLive(); + Status nodeStatus = getStatus(); + if (nodeStatus == Status.NEW) { + return (hasProperty(name)) ? Status.NEW : null; + } else if (nodeStatus == Status.REMOVED) { + return Status.REMOVED; // FIXME not correct if no property existed with that name + } else { + PropertyState head = internalGetProperty(name); + if (head != null && !canRead(head)) { + // no permission to read status information for existing property + return null; + } + + PropertyState base = getBaseState().getProperty(name); + if (head == null) { + return (base == null) ? null : Status.REMOVED; + } else { + if (base == null) { + return Status.NEW; + } else if (head.equals(base)) { + return Status.EXISTING; + } else { + return Status.MODIFIED; + } + } + } + } + + @Override + public boolean hasProperty(String name) { + root.checkLive(); + return getProperty(name) != null; + } + + @Override + public long getPropertyCount() { + root.checkLive(); + return Iterables.size(getProperties()); + } + + @Override + public Iterable getProperties() { + root.checkLive(); + return Iterables.filter(getNodeBuilder().getProperties(), + new Predicate() { + @Override + public boolean apply(PropertyState propertyState) { + return canRead(propertyState); + } + }); + } + + @Override + public TreeImpl getChild(String name) { + root.checkLive(); + TreeImpl child = internalGetChild(name); + if (child != null && canRead(child)) { + return child; + } else { + return null; + } + } + + @Override + public Status getStatus() { + root.checkLive(); + if (isRemoved()) { + return Status.REMOVED; + } + + NodeState baseState = getBaseState(); + if (baseState == null) { + // Did not exist before, so its NEW + return Status.NEW; + } else { + // Did exit it before. So... + if (isSame(baseState, getNodeState())) { + // ...it's EXISTING if it hasn't changed + return Status.EXISTING; + } else { + // ...and MODIFIED otherwise. + return Status.MODIFIED; + } + } + } + + @Override + public boolean hasChild(String name) { + root.checkLive(); + return getChild(name) != null; + } + + @Override + public long getChildrenCount() { + // TODO: make sure cnt respects access control + root.checkLive(); + return getNodeBuilder().getChildNodeCount(); + } + + @Override + public Iterable getChildren() { + root.checkLive(); + Iterable childNames; + if (hasOrderableChildren()) { + childNames = getOrderedChildNames(); + } else { + childNames = getNodeBuilder().getChildNodeNames(); + } + return Iterables.filter(Iterables.transform( + childNames, + new Function() { + @Override + public Tree apply(String input) { + return new TreeImpl(root, TreeImpl.this, input); + } + }), + new Predicate() { + @Override + public boolean apply(Tree tree) { + return tree != null && canRead(tree); + } + }); + } + + @Override + public Tree addChild(String name) { + root.checkLive(); + if (!hasChild(name)) { + getNodeBuilder().child(name); + if (hasOrderableChildren()) { + getNodeBuilder().setProperty( + MemoryPropertyBuilder.create(Type.STRING, internalGetProperty(OAK_CHILD_ORDER)) + .addValue(name) + .getPropertyState(true)); + } + root.updated(); + } + + TreeImpl child = getChild(name); + assert child != null; + return child; + } + + @Override + public boolean remove() { + root.checkLive(); + if (isRemoved()) { + throw new IllegalStateException("Cannot remove removed tree"); + } + + if (!isRoot() && parent.hasChild(name)) { + NodeBuilder builder = parent.getNodeBuilder(); + builder.removeNode(name); + removed = true; + if (parent.hasOrderableChildren()) { + builder.setProperty( + MemoryPropertyBuilder.create(Type.STRING, parent.internalGetProperty(OAK_CHILD_ORDER)) + .removeValue(name) + .getPropertyState(true) + ); + } + root.updated(); + return true; + } else { + return false; + } + } + + @Override + public boolean orderBefore(final String name) { + root.checkLive(); + if (isRoot()) { + // root does not have siblings + return false; + } + if (name != null && !parent.hasChild(name)) { + // so such sibling or not accessible + return false; + } + // perform the reorder + parent.ensureChildOrderProperty(); + // all siblings but not this one + Iterable filtered = Iterables.filter( + parent.getOrderedChildNames(), + new Predicate() { + @Override + public boolean apply(@Nullable String input) { + return !TreeImpl.this.getName().equals(input); + } + }); + // create head and tail + Iterable head; + Iterable tail; + if (name == null) { + head = filtered; + tail = Collections.emptyList(); + } else { + int idx = Iterables.indexOf(filtered, new Predicate() { + @Override + public boolean apply(@Nullable String input) { + return name.equals(input); + } + }); + head = Iterables.limit(filtered, idx); + tail = Iterables.skip(filtered, idx); + } + // concatenate head, this name and tail + parent.getNodeBuilder().setProperty(MultiStringPropertyState.stringProperty(OAK_CHILD_ORDER, Iterables.concat(head, Collections.singleton(getName()), tail)) + ); + root.updated(); + return true; + } + + @Override + public void setProperty(PropertyState property) { + root.checkLive(); + NodeBuilder builder = getNodeBuilder(); + builder.setProperty(property); + root.updated(); + } + + @Override + public void setProperty(String name, T value) { + root.checkLive(); + NodeBuilder builder = getNodeBuilder(); + builder.setProperty(name, value); + root.updated(); + } + + @Override + public void setProperty(String name, T value, Type type) { + root.checkLive(); + NodeBuilder builder = getNodeBuilder(); + builder.setProperty(name, value, type); + root.updated(); + } + + @Override + public void removeProperty(String name) { + root.checkLive(); + NodeBuilder builder = getNodeBuilder(); + builder.removeProperty(name); + root.updated(); + } + + @Override + public TreeLocation getLocation() { + root.checkLive(); + return new NodeLocation(this); + } + + //--------------------------------------------------< RootImpl.Listener >--- + + @Override + public void purged() { + nodeBuilder = null; + } + + //----------------------------------------------------------< protected >--- + + @CheckForNull + protected NodeState getBaseState() { + if (isRemoved()) { + throw new IllegalStateException("Cannot get the base state of a removed tree"); + } + + NodeState parentBaseState = parent.getBaseState(); + return parentBaseState == null + ? null + : parentBaseState.getChildNode(name); + } + + @Nonnull + protected synchronized NodeBuilder getNodeBuilder() { + if (isRemoved()) { + throw new IllegalStateException("Cannot get a builder for a removed tree"); + } + + if (nodeBuilder == null) { + nodeBuilder = parent.getNodeBuilder().child(name); + root.addListener(this); + } + return nodeBuilder; + } + + //-----------------------------------------------------------< internal >--- + + /** + * Move this tree to the parent at {@code destParent} with the new name + * {@code destName}. + * + * @param destParent new parent for this tree + * @param destName new name for this tree + */ + void moveTo(TreeImpl destParent, String destName) { + if (isRemoved()) { + throw new IllegalStateException("Cannot move removed tree"); + } + + name = destName; + parent = destParent; + } + + @Nonnull + NodeState getNodeState() { + return getNodeBuilder().getNodeState(); + } + + /** + * Get a tree for the tree identified by {@code path}. + * + * @param path the path to the child + * @return a {@link Tree} instance for the child at {@code path} or + * {@code null} if no such tree exits or if the tree is not accessible. + */ + @CheckForNull + TreeImpl getTree(String path) { + checkArgument(path.startsWith("/")); + TreeImpl child = this; + for (String name : elements(path)) { + child = child.internalGetChild(name); + if (child == null) { + return null; + } + } + return (canRead(child)) ? child : null; + } + + /** + * Update the child order with children that have been removed or added. + * Added children are appended to the end of the {@link #OAK_CHILD_ORDER} + * property. + */ + void updateChildOrder() { + if (!hasOrderableChildren()) { + return; + } + Set names = Sets.newLinkedHashSet(); + for (String name : getOrderedChildNames()) { + if (getNodeBuilder().hasChildNode(name)) { + names.add(name); + } + } + for (String name : getNodeBuilder().getChildNodeNames()) { + names.add(name); + } + PropertyBuilder builder = MemoryPropertyBuilder.create( + Type.STRING, OAK_CHILD_ORDER); + builder.setValues(names); + getNodeBuilder().setProperty(builder.getPropertyState(true)); + } + + //------------------------------------------------------------< private >--- + + private TreeImpl internalGetChild(String childName) { + return getNodeBuilder().hasChildNode(childName) + ? new TreeImpl(root, this, childName) + : null; + } + + private PropertyState internalGetProperty(String propertyName) { + return getNodeBuilder().getProperty(propertyName); + } + + private boolean isRemoved() { + return removed || (parent != null && parent.isRemoved()); + } + + private void buildPath(StringBuilder sb) { + if (!isRoot()) { + parent.buildPath(sb); + sb.append('/').append(name); + } + } + + private boolean canRead(Tree tree) { + // FIXME: access control eval must have full access to the tree + // FIXME: special handling for access control item and version content + return root.getPermissions().canRead(tree); + } + + private boolean canRead(PropertyState property) { + // FIXME: access control eval must have full access to the tree/property + // FIXME: special handling for access control item and version content + return (property != null) + && root.getPermissions().canRead(this, property) + && !NodeStateUtils.isHidden(property.getName()); + } + + /** + * @return {@code true} if this tree has orderable children; + * {@code false} otherwise. + */ + private boolean hasOrderableChildren() { + return internalGetProperty(OAK_CHILD_ORDER) != null; + } + + /** + * Returns the ordered child names. This method must only be called when + * this tree {@link #hasOrderableChildren()}. + * + * @return the ordered child names. + */ + private Iterable getOrderedChildNames() { + assert hasOrderableChildren(); + return new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + final PropertyState childOrder = internalGetProperty(OAK_CHILD_ORDER); + int index = 0; + + @Override + public boolean hasNext() { + return index < childOrder.count(); + } + + @Override + public String next() { + return childOrder.getValue(Type.STRING, index++); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + + /** + * Ensures that the {@link #OAK_CHILD_ORDER} exists. This method will create + * the property if it doesn't exist and initialize the value with the names + * of the children as returned by {@link NodeBuilder#getChildNodeNames()}. + */ + private void ensureChildOrderProperty() { + PropertyState childOrder = getNodeBuilder().getProperty(OAK_CHILD_ORDER); + if (childOrder == null) { + getNodeBuilder().setProperty( + MultiStringPropertyState.stringProperty(OAK_CHILD_ORDER, getNodeBuilder().getChildNodeNames())); + } + } + + private static boolean isSame(NodeState state1, NodeState state2) { + final boolean[] isDirty = {false}; + state2.compareAgainstBaseState(state1, new NodeStateDiff() { + @Override + public void propertyAdded(PropertyState after) { + isDirty[0] = true; + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) { + isDirty[0] = true; + } + + @Override + public void propertyDeleted(PropertyState before) { + isDirty[0] = true; + } + + @Override + public void childNodeAdded(String name, NodeState after) { + isDirty[0] = true; + } + + @Override + public void childNodeChanged(String name, NodeState before, NodeState after) { + // cut transitivity here + } + + @Override + public void childNodeDeleted(String name, NodeState before) { + isDirty[0] = true; + } + }); + + return !isDirty[0]; + } + + //------------------------------------------------------------< TreeLocation >--- + + public class NodeLocation implements TreeLocation { + private final TreeImpl tree; + + private NodeLocation(TreeImpl tree) { + this.tree = checkNotNull(tree); + } + + @Override + public TreeLocation getParent() { + return tree.parent == null + ? NullLocation.INSTANCE + : new NodeLocation(tree.parent); + } + + @Override + public TreeLocation getChild(String relPath) { + checkArgument(!relPath.startsWith("/")); + if (relPath.isEmpty()) { + return this; + } + + TreeImpl child = tree; + String parentPath = PathUtils.getParentPath(relPath); + for (String name : PathUtils.elements(parentPath)) { + child = child.internalGetChild(name); + if (child == null) { + return NullLocation.INSTANCE; + } + } + + String name = PathUtils.getName(relPath); + PropertyState property = child.internalGetProperty(name); + if (property != null) { + return new PropertyLocation(new NodeLocation(child), property); + } + else { + child = child.internalGetChild(name); + return child == null + ? NullLocation.INSTANCE + : new NodeLocation(child); + } + } + + @Override + public String getPath() { + return tree.getPath(); + } + + @Override + public Tree getTree() { + return canRead(tree) ? tree : null; + } + + @Override + public PropertyState getProperty() { + return null; + } + + @Override + public Status getStatus() { + return tree.getStatus(); + } + } + + public class PropertyLocation implements TreeLocation { + private final NodeLocation parent; + private final PropertyState property; + + private PropertyLocation(NodeLocation parent, PropertyState property) { + this.parent = checkNotNull(parent); + this.property = checkNotNull(property); + } + + @Override + public TreeLocation getParent() { + return parent; + } + + @Override + public TreeLocation getChild(String relPath) { + return NullLocation.INSTANCE; + } + + @Override + public String getPath() { + return PathUtils.concat(parent.getPath(), property.getName()); + } + + @Override + public Tree getTree() { + return null; + } + + @Override + public PropertyState getProperty() { + return canRead(property) + ? property + : null; + } + + @Override + public Status getStatus() { + return parent.tree.getPropertyStatus(property.getName()); + } + + /** + * Set the underlying property + * @param property The property to set + */ + public void set(PropertyState property) { + parent.tree.setProperty(property); + } + + /** + * Remove the underlying property + * @return {@code true} on success false otherwise + */ + public boolean remove() { + parent.tree.removeProperty(property.getName()); + return true; + } + } + + public static class NullLocation implements TreeLocation { + public static final NullLocation INSTANCE = new NullLocation(); + + private NullLocation() { + } + + @Override + public TreeLocation getParent() { + return this; + } + + @Override + public TreeLocation getChild(String relPath) { + return this; + } + + @Override + public String getPath() { + return null; + } + + @Override + public Tree getTree() { + return null; + } + + @Override + public PropertyState getProperty() { + return null; + } + + @Override + public Status getStatus() { + return null; + } + } + +} + + diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/JsopDiff.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/JsopDiff.java new file mode 100644 index 00000000000..fdcd7a4e597 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/JsopDiff.java @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.kernel; + +import java.io.IOException; +import java.io.InputStream; + +import javax.jcr.PropertyType; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.json.JsopBuilder; +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.jackrabbit.oak.api.Type.BINARIES; +import static org.apache.jackrabbit.oak.api.Type.BOOLEANS; +import static org.apache.jackrabbit.oak.api.Type.LONGS; +import static org.apache.jackrabbit.oak.api.Type.STRINGS; + +class JsopDiff implements NodeStateDiff { + private static final Logger log = LoggerFactory.getLogger(JsopDiff.class); + + private final MicroKernel kernel; + + protected final JsopBuilder jsop; + + protected final String path; + + public JsopDiff(MicroKernel kernel, JsopBuilder jsop, String path) { + this.kernel = kernel; + this.jsop = jsop; + this.path = path; + } + + public JsopDiff(MicroKernel kernel) { + this(kernel, new JsopBuilder(), "/"); + } + + public static void diffToJsop( + MicroKernel kernel, NodeState before, NodeState after, + String path, JsopBuilder jsop) { + after.compareAgainstBaseState(before, new JsopDiff(kernel, jsop, path)); + } + + protected JsopDiff createChildDiff(JsopBuilder jsop, String path) { + return new JsopDiff(kernel, jsop, path); + } + + //-----------------------------------------------------< NodeStateDiff >-- + + @Override + public void propertyAdded(PropertyState after) { + jsop.tag('^').key(buildPath(after.getName())); + toJson(after, jsop); + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) { + jsop.tag('^').key(buildPath(after.getName())); + toJson(after, jsop); + } + + @Override + public void propertyDeleted(PropertyState before) { + jsop.tag('^').key(buildPath(before.getName())).value(null); + } + + @Override + public void childNodeAdded(String name, NodeState after) { + jsop.tag('+').key(buildPath(name)); + toJson(after, jsop); + } + + @Override + public void childNodeDeleted(String name, NodeState before) { + jsop.tag('-').value(buildPath(name)); + } + + @Override + public void childNodeChanged(String name, NodeState before, NodeState after) { + String path = buildPath(name); + after.compareAgainstBaseState(before, createChildDiff(jsop, path)); + } + + //------------------------------------------------------------< Object >-- + + @Override + public String toString() { + return jsop.toString(); + } + + //-----------------------------------------------------------< private >-- + + protected String buildPath(String name) { + return PathUtils.concat(path, name); + } + + private void toJson(NodeState nodeState, JsopBuilder jsop) { + jsop.object(); + for (PropertyState property : nodeState.getProperties()) { + jsop.key(property.getName()); + toJson(property, jsop); + } + for (ChildNodeEntry child : nodeState.getChildNodeEntries()) { + jsop.key(child.getName()); + toJson(child.getNodeState(), jsop); + } + jsop.endObject(); + } + + private void toJson(PropertyState propertyState, JsopBuilder jsop) { + if (propertyState.isArray()) { + jsop.array(); + toJsonValue(propertyState, jsop); + jsop.endArray(); + } else { + toJsonValue(propertyState, jsop); + } + } + + private void toJsonValue(PropertyState property, JsopBuilder jsop) { + int type = property.getType().tag(); + switch (type) { + case PropertyType.BOOLEAN: + for (boolean value : property.getValue(BOOLEANS)) { + jsop.value(value); + } + break; + case PropertyType.LONG: + for (long value : property.getValue(LONGS)) { + jsop.value(value); + } + break; + case PropertyType.BINARY: + for (Blob value : property.getValue(BINARIES)) { + InputStream is = value.getNewStream(); + String binId = TypeCodes.getCodeForType(type) + ':' + kernel.write(is); + close(is); + jsop.value(binId); + } + break; + default: + for (String value : property.getValue(STRINGS)) { + if (PropertyType.STRING != type || TypeCodes.startsWithCode(value)) { + value = TypeCodes.getCodeForType(type) + ':' + value; + } + jsop.value(value); + } + break; + } + } + + private static void close(InputStream stream) { + try { + stream.close(); + } + catch (IOException e) { + log.warn("Error closing stream", e); + } + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelBlob.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelBlob.java new file mode 100644 index 00000000000..49c08df7551 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelBlob.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.kernel; + +import java.io.InputStream; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.util.MicroKernelInputStream; +import org.apache.jackrabbit.oak.plugins.memory.AbstractBlob; + +/** + * This {@code Blob} implementation is backed by a binary stored in + * a {@code MicroKernel}. + */ +public class KernelBlob extends AbstractBlob { + private final String binaryID; + private final MicroKernel kernel; + + /** + * Create a new instance for a binary id and a Microkernel. + * @param binaryID id of the binary + * @param kernel + */ + public KernelBlob(String binaryID, MicroKernel kernel) { + this.binaryID = binaryID; + this.kernel = kernel; + } + + @Nonnull + @Override + public InputStream getNewStream() { + return new MicroKernelInputStream(kernel, binaryID); + } + + /** + * This implementation delegates the calculation of the length back + * to the underlying {@code MicroKernel}. + */ + @Override + public long length() { + return kernel.getLength(binaryID); + } + + /** + * This implementation delegates back to the underlying {@code Microkernel} + * if other is also of type {@code KernelBlob}. + */ + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other instanceof KernelBlob) { + KernelBlob that = (KernelBlob) other; + return binaryID.equals(that.binaryID); + } + + return super.equals(other); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelNodeBuilder.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelNodeBuilder.java new file mode 100644 index 00000000000..0b8187f7997 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelNodeBuilder.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.kernel; + +import static com.google.common.base.Preconditions.checkNotNull; + +import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeBuilder; + +class KernelNodeBuilder extends MemoryNodeBuilder { + + private final KernelRootBuilder root; + + public KernelNodeBuilder( + MemoryNodeBuilder parent, String name, KernelRootBuilder root) { + super(parent, name); + this.root = checkNotNull(root); + } + + //--------------------------------------------------< MemoryNodeBuilder >--- + + @Override + protected MemoryNodeBuilder createChildBuilder(String name) { + return new KernelNodeBuilder(this, name, root); + } + + @Override + protected void updated() { + root.updated(); + } + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelNodeState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelNodeState.java new file mode 100644 index 00000000000..3895fd045e7 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelNodeState.java @@ -0,0 +1,384 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.kernel; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ExecutionException; + +import javax.annotation.Nonnull; + +import com.google.common.base.Function; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.api.MicroKernelException; +import org.apache.jackrabbit.mk.json.JsopReader; +import org.apache.jackrabbit.mk.json.JsopTokenizer; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeBuilder; +import org.apache.jackrabbit.oak.spi.state.AbstractChildNodeEntry; +import org.apache.jackrabbit.oak.spi.state.AbstractNodeState; +import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.apache.jackrabbit.oak.plugins.memory.PropertyStates.readArrayProperty; +import static org.apache.jackrabbit.oak.plugins.memory.PropertyStates.readProperty; + +/** + * Basic {@link NodeState} implementation based on the {@link MicroKernel} + * interface. This class makes an attempt to load data lazily. + */ +public final class KernelNodeState extends AbstractNodeState { + + /** + * Maximum number of child nodes kept in memory. + */ + static final int MAX_CHILD_NODE_NAMES = 1000; + + private final MicroKernel kernel; + + private final String path; + + private final String revision; + + private Map properties; + + private long childNodeCount = -1; + + private String hash; + + private Map childPaths; + + private final LoadingCache cache; + + /** + * Create a new instance of this class representing the node at the + * given {@code path} and {@code revision}. It is an error if the + * underlying Microkernel does not contain such a node. + * + * @param kernel the underlying MicroKernel + * @param path the path of this KernelNodeState + * @param revision the revision of the node to read from the kernel. + * @param cache the KernelNodeState cache + */ + public KernelNodeState( + MicroKernel kernel, String path, String revision, + LoadingCache cache) { + this.kernel = checkNotNull(kernel); + this.path = checkNotNull(path); + this.revision = checkNotNull(revision); + this.cache = checkNotNull(cache); + } + + private synchronized void init() { + if (properties == null) { + String json = kernel.getNodes( + path, revision, 0, 0, MAX_CHILD_NODE_NAMES, + "{\"properties\":[\"*\",\":hash\"]}"); + + JsopReader reader = new JsopTokenizer(json); + reader.read('{'); + properties = new LinkedHashMap(); + childPaths = new LinkedHashMap(); + do { + String name = reader.readString(); + reader.read(':'); + if (":childNodeCount".equals(name)) { + childNodeCount = + Long.valueOf(reader.read(JsopReader.NUMBER)); + } else if (":hash".equals(name)) { + hash = reader.read(JsopReader.STRING); + } else if (reader.matches('{')) { + reader.read('}'); + String childPath = path + '/' + name; + if ("/".equals(path)) { + childPath = '/' + name; + } + childPaths.put(name, childPath); + } else if (reader.matches('[')) { + properties.put(name, readArrayProperty(name, reader, kernel)); + } else { + properties.put(name, readProperty(name, reader, kernel)); + } + } while (reader.matches(',')); + reader.read('}'); + reader.read(JsopReader.END); + // optimize for empty childNodes + if (childPaths.isEmpty()) { + childPaths = Collections.emptyMap(); + } + } + } + + @Override + public long getPropertyCount() { + init(); + return properties.size(); + } + + @Override + public PropertyState getProperty(String name) { + init(); + return properties.get(name); + } + + @Override + public Iterable getProperties() { + init(); + return properties.values(); + } + + @Override + public long getChildNodeCount() { + init(); + return childNodeCount; + } + + @Override + public NodeState getChildNode(String name) { + init(); + String childPath = childPaths.get(name); + if (childPath == null && childNodeCount > MAX_CHILD_NODE_NAMES) { + String path = getChildPath(name); + if (kernel.nodeExists(path, revision)) { + childPath = path; + } + } + if (childPath == null) { + return null; + } + try { + return cache.get(revision + childPath); + } catch (ExecutionException e) { + throw new MicroKernelException(e); + } + } + + @Override + public Iterable getChildNodeEntries() { + init(); + Iterable iterable = iterable(childPaths.entrySet()); + if (childNodeCount > childPaths.size()) { + List> iterables = Lists.newArrayList(); + iterables.add(iterable); + long offset = childPaths.size(); + while (offset < childNodeCount) { + iterables.add(getChildNodeEntries(offset, MAX_CHILD_NODE_NAMES)); + offset += MAX_CHILD_NODE_NAMES; + } + iterable = Iterables.concat(iterables); + } + return iterable; + } + + @Override + public NodeBuilder builder() { + if ("/".equals(getPath())) { + return new KernelRootBuilder(kernel, this); + } else { + return new MemoryNodeBuilder(this); + } + } + + /** + * Optimised comparison method that can avoid traversing all properties + * and child nodes if both this and the given base node state come from + * the same MicroKernel and either have the same content hash (when + * available) or are located at the same path in different revisions. + * + * @see OAK-175 + */ + @Override + public void compareAgainstBaseState(NodeState base, NodeStateDiff diff) { + if (this == base) { + return; // no differences + } else if (base instanceof KernelNodeState) { + KernelNodeState kbase = (KernelNodeState) base; + if (kernel.equals(kbase.kernel)) { + if (revision.equals(kbase.revision) && path.equals(kbase.path)) { + return; // no differences + } else { + init(); + kbase.init(); + if (hash != null && hash.equals(kbase.hash)) { + return; // no differences + } else if (path.equals(kbase.path)) { + // TODO: Parse the JSON diff returned by the kernel + // kernel.diff(kbase.revision, revision, path); + } + } + } + } + // fall back to the generic node state diff algorithm + super.compareAgainstBaseState(base, diff); + } + + //------------------------------------------------------------< Object >-- + + /** + * Optimised equality check that can avoid a full tree comparison if + * both instances come from the same MicroKernel and have either + * the same revision and path or the same content hash (when available). + * Otherwise we fall back to the default tree comparison algorithm. + * + * @see OAK-172 + */ + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } else if (object instanceof KernelNodeState) { + KernelNodeState that = (KernelNodeState) object; + if (kernel.equals(that.kernel)) { + if (revision.equals(that.revision) && path.equals(that.path)) { + return true; + } else { + this.init(); + that.init(); + if (hash != null && that.hash != null) { + return hash.equals(that.hash); + } + } + } + } + // fall back to the generic tree equality comparison algorithm + return super.equals(object); + } + + //------------------------------------------------------------< internal >--- + + @Nonnull + String getRevision() { + return revision; + } + + @Nonnull + String getPath() { + return path; + } + + //------------------------------------------------------------< private >--- + + private Iterable getChildNodeEntries( + final long offset, final int count) { + return new Iterable() { + @Override + public Iterator iterator() { + List entries = + Lists.newArrayListWithCapacity(count); + String json = kernel.getNodes( + path, revision, 0, offset, count, null); + JsopReader reader = new JsopTokenizer(json); + reader.read('{'); + do { + String name = reader.readString(); + reader.read(':'); + if (reader.matches('{')) { + reader.read('}'); + String childPath = getChildPath(name); + entries.add(new KernelChildNodeEntry(name, childPath)); + } else if (reader.matches('[')) { + while (reader.read() != ']') { + // skip + } + } else { + reader.read(); + } + } while (reader.matches(',')); + reader.read('}'); + reader.read(JsopReader.END); + return entries.iterator(); + } + }; + } + + private String getChildPath(String name) { + if ("/".equals(path)) { + return '/' + name; + } else { + return path + '/' + name; + } + } + + private Iterable iterable( + Iterable> set) { + return Iterables.transform( + set, + new Function, ChildNodeEntry>() { + @Override + public ChildNodeEntry apply(Entry input) { + return new KernelChildNodeEntry(input); + } + }); + } + + private class KernelChildNodeEntry extends AbstractChildNodeEntry { + + private final String name; + + private final String path; + + /** + * Creates a child node entry with the given name and referenced + * child node state. + * + * @param name child node name + * @param path child node path + */ + public KernelChildNodeEntry(String name, String path) { + this.name = checkNotNull(name); + this.path = checkNotNull(path); + } + + /** + * Utility constructor that copies the name and referenced + * child node state from the given map entry. + * + * @param entry map entry + */ + public KernelChildNodeEntry(Map.Entry entry) { + this(entry.getKey(), entry.getValue()); + } + + @Override + public String getName() { + return name; + } + + @Override + public NodeState getNodeState() { + try { + return cache.get(revision + path); + } catch (ExecutionException e) { + throw new MicroKernelException(e); + } + } + + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelNodeStore.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelNodeStore.java new file mode 100644 index 00000000000..299207da0c8 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelNodeStore.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.kernel; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.ExecutionException; + +import javax.annotation.Nonnull; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.api.MicroKernelException; +import org.apache.jackrabbit.oak.spi.commit.CommitHook; +import org.apache.jackrabbit.oak.spi.commit.EmptyHook; +import org.apache.jackrabbit.oak.spi.commit.EmptyObserver; +import org.apache.jackrabbit.oak.spi.commit.Observer; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStore; +import org.apache.jackrabbit.oak.spi.state.NodeStoreBranch; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * {@code NodeStore} implementations against {@link MicroKernel}. + */ +public class KernelNodeStore implements NodeStore { + + /** + * The {@link MicroKernel} instance used to store the content tree. + */ + private final MicroKernel kernel; + + /** + * Commit hook. + */ + @Nonnull + private volatile CommitHook hook = EmptyHook.INSTANCE; + + /** + * Change observer. + */ + @Nonnull + private volatile Observer observer = EmptyObserver.INSTANCE; + + private final LoadingCache cache = + CacheBuilder.newBuilder().maximumSize(10000).build( + new CacheLoader() { + @Override + public KernelNodeState load(String key) { + int slash = key.indexOf('/'); + String revision = key.substring(0, slash); + String path = key.substring(slash); + return new KernelNodeState( + kernel, path, revision, cache); + } + }); + + /** + * State of the current root node. + */ + private KernelNodeState root; + + public KernelNodeStore(MicroKernel kernel) { + this.kernel = checkNotNull(kernel); + try { + this.root = cache.get(kernel.getHeadRevision() + "/"); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Nonnull + public CommitHook getHook() { + return hook; + } + + public void setHook(CommitHook hook) { + this.hook = checkNotNull(hook); + } + + @Nonnull + public Observer getObserver() { + return observer; + } + + public void setObserver(Observer observer) { + this.observer = checkNotNull(observer); + } + + //----------------------------------------------------------< NodeStore >--- + + @Override + public synchronized KernelNodeState getRoot() { + String revision = kernel.getHeadRevision(); + if (!revision.equals(root.getRevision())) { + NodeState before = root; + root = getRootState(revision); + observer.contentChanged(before, root); + } + return root; + } + + @Override + public NodeStoreBranch branch() { + return new KernelNodeStoreBranch(this, getRoot()); + } + + /** + * @return An instance of {@link KernelBlob} + */ + @Override + public KernelBlob createBlob(InputStream inputStream) throws IOException { + try { + String blobId = kernel.write(inputStream); + return new KernelBlob(blobId, kernel); + } + catch (MicroKernelException e) { + throw new IOException(e); + } + } + + //-----------------------------------------------------------< internal >--- + + @Nonnull + MicroKernel getKernel() { + return kernel; + } + + KernelNodeState getRootState(String revision) { + try { + return cache.get(revision + "/"); + } catch (ExecutionException e) { + throw new MicroKernelException(e); + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelNodeStoreBranch.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelNodeStoreBranch.java new file mode 100644 index 00000000000..410e32792dd --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelNodeStoreBranch.java @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.kernel; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.api.MicroKernelException; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.spi.commit.CommitHook; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStoreBranch; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.apache.jackrabbit.oak.commons.PathUtils.elements; +import static org.apache.jackrabbit.oak.commons.PathUtils.getName; +import static org.apache.jackrabbit.oak.commons.PathUtils.getParentPath; + +/** + * {@code NodeStoreBranch} based on {@link MicroKernel} branching and merging. + * This implementation keeps changes in memory up to a certain limit and writes + * them back when the to the Microkernel branch when the limit is exceeded. + */ +class KernelNodeStoreBranch implements NodeStoreBranch { + + /** The underlying store to which this branch belongs */ + private final KernelNodeStore store; + + /** Base state of this branch */ + private final NodeState base; + + /** Revision from which to branch */ + private final String headRevision; + + /** Revision of this branch in the Microkernel, null if not yet branched */ + private String branchRevision; + + /** Current root state of this branch */ + private NodeState currentRoot; + + /** Last state which was committed to this branch */ + private NodeState committed; + + KernelNodeStoreBranch(KernelNodeStore store, KernelNodeState root) { + this.store = store; + this.headRevision = root.getRevision(); + this.currentRoot = root; + this.base = currentRoot; + this.committed = currentRoot; + } + + @Override + public NodeState getBase() { + return base; + } + + @Override + public NodeState getRoot() { + return currentRoot; + } + + @Override + public void setRoot(NodeState newRoot) { + if (!currentRoot.equals(newRoot)) { + currentRoot = newRoot; + JsopDiff diff = new JsopDiff(store.getKernel()); + currentRoot.compareAgainstBaseState(committed, diff); + commit(diff.toString()); + } + } + + @Override + public boolean move(String source, String target) { + if (getNode(source) == null) { + // source does not exist + return false; + } + NodeState destParent = getNode(getParentPath(target)); + if (destParent == null) { + // parent of destination does not exist + return false; + } + if (destParent.getChildNode(getName(target)) != null) { + // destination exists already + return false; + } + + commit(">\"" + source + "\":\"" + target + '"'); + return true; + } + + @Override + public boolean copy(String source, String target) { + if (getNode(source) == null) { + // source does not exist + return false; + } + NodeState destParent = getNode(getParentPath(target)); + if (destParent == null) { + // parent of destination does not exist + return false; + } + if (destParent.getChildNode(getName(target)) != null) { + // destination exists already + return false; + } + + commit("*\"" + source + "\":\"" + target + '"'); + return true; + } + + @Override + public NodeState merge() throws CommitFailedException { + NodeState oldRoot = base; + CommitHook commitHook = store.getHook(); + NodeState toCommit = commitHook.processCommit(oldRoot, currentRoot); + setRoot(toCommit); + + try { + if (branchRevision == null) { + // Nothing was written to this branch: return initial node state. + branchRevision = null; + currentRoot = null; + return committed; + } + else { + MicroKernel kernel = store.getKernel(); + String mergedRevision = kernel.merge(branchRevision, null); + branchRevision = null; + currentRoot = null; + return store.getRootState(mergedRevision); + } + } + catch (MicroKernelException e) { + throw new CommitFailedException(e); + } + } + + //------------------------------------------------------------< private >--- + + private NodeState getNode(String path) { + checkArgument(path.startsWith("/")); + NodeState node = getRoot(); + for (String name : elements(path)) { + node = node.getChildNode(name); + if (node == null) { + break; + } + } + + return node; + } + + private void commit(String jsop) { + MicroKernel kernel = store.getKernel(); + if (branchRevision == null) { + // create the branch if this is the first commit + branchRevision = kernel.branch(headRevision); + } + + branchRevision = kernel.commit("", jsop, branchRevision, null); + currentRoot = store.getRootState(branchRevision); + committed = currentRoot; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelRootBuilder.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelRootBuilder.java new file mode 100644 index 00000000000..ecb5427fe84 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelRootBuilder.java @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.kernel; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.apache.jackrabbit.oak.plugins.memory.ModifiedNodeState.collapse; + +import java.util.Map; +import java.util.Set; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.json.JsopBuilder; +import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeBuilder; +import org.apache.jackrabbit.oak.plugins.memory.ModifiedNodeState; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + +class KernelRootBuilder extends MemoryNodeBuilder { + + /** + * Number of content updates that need to happen before the updates + * are automatically committed to a branch in the MicroKernel. + */ + private static final int UPDATE_LIMIT = 10000; + + private final MicroKernel kernel; + + private String baseRevision; + + private String branchRevision; + + private int updates = 0; + + public KernelRootBuilder(MicroKernel kernel, KernelNodeState state) { + super(checkNotNull(state)); + this.kernel = checkNotNull(kernel); + this.baseRevision = state.getRevision(); + this.branchRevision = null; + } + + //--------------------------------------------------< MemoryNodeBuilder >--- + + @Override + protected MemoryNodeBuilder createChildBuilder(String name) { + return new KernelNodeBuilder(this, name, this); + } + + @Override + protected void updated() { + if (updates++ > UPDATE_LIMIT) { + CopyAndMoveAwareJsopDiff diff = new CopyAndMoveAwareJsopDiff(); + compareAgainstBaseState(diff); + diff.processMovesAndCopies(); + + if (branchRevision == null) { + branchRevision = kernel.branch(baseRevision); + } + branchRevision = kernel.commit( + "/", diff.toString(), branchRevision, null); + + updates = 0; + } + } + + private class CopyAndMoveAwareJsopDiff extends JsopDiff { + + private final Map added; + + + private final Set deleted; + + public CopyAndMoveAwareJsopDiff() { + super(kernel); + added = Maps.newHashMap(); + deleted = Sets.newHashSet(); + } + + private CopyAndMoveAwareJsopDiff( + JsopBuilder jsop, String path, + Map added, Set deleted) { + super(kernel, jsop, path); + this.added = added; + this.deleted = deleted; + } + + public void processMovesAndCopies() { + for (Map.Entry entry : added.entrySet()) { + NodeState state = entry.getValue(); + String path = entry.getKey(); + + KernelNodeState kstate = getKernelBaseState(state); + String kpath = kstate.getPath(); + + if (deleted.remove(kpath)) { + jsop.tag('>'); + } else { + jsop.tag('*'); + } + jsop.key(kpath).value(path); + + if (state != kstate) { + state.compareAgainstBaseState( + kstate, new JsopDiff(kernel, jsop, path)); + } + } + + for (String path : deleted) { + jsop.tag('-').value(path); + } + } + + //------------------------------------------------------< JsopDiff >-- + + @Override + protected JsopDiff createChildDiff(JsopBuilder jsop, String path) { + return new CopyAndMoveAwareJsopDiff(jsop, path, added, deleted); + } + + //-------------------------------------------------< NodeStateDiff >-- + + @Override + public void childNodeAdded(String name, NodeState after) { + KernelNodeState kstate = getKernelBaseState(after); + if (kstate != null) { + added.put(buildPath(name), after); + } else { + super.childNodeAdded(name, after); + } + } + + + @Override + public void childNodeChanged( + String name, NodeState before, NodeState after) { + KernelNodeState kstate = getKernelBaseState(after); + String path = buildPath(name); + if (kstate != null && !path.equals(kstate.getPath())) { + deleted.add(path); + added.put(path, after); + } else { + super.childNodeChanged(name, before, after); + } + } + + @Override + public void childNodeDeleted(String name, NodeState before) { + deleted.add(buildPath(name)); + } + + //-------------------------------------------------------< private >-- + + private KernelNodeState getKernelBaseState(NodeState state) { + if (state instanceof ModifiedNodeState) { + state = collapse((ModifiedNodeState) state).getBaseState(); + } + + if (state instanceof KernelNodeState) { + KernelNodeState kstate = (KernelNodeState) state; + String arev = kstate.getRevision(); + String brev = branchRevision; + if (brev == null) { + brev = baseRevision; + } + if (arev.equals(brev)) { + return kstate; + } + } + + return null; + } + + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/TypeCodes.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/TypeCodes.java new file mode 100644 index 00000000000..bc8f8738e13 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/TypeCodes.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.kernel; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import javax.jcr.PropertyType; + +/** + * TypeCodes maps between {@code Type} and the code used to prefix + * its json serialisation. + */ +public class TypeCodes { + private static final Map TYPE2CODE = new HashMap(); + private static final Map CODE2TYPE = new HashMap(); + + static { + for (int type = PropertyType.UNDEFINED; type <= PropertyType.DECIMAL; type++) { + String code = PropertyType.nameFromValue(type).substring(0, 3).toLowerCase(Locale.ENGLISH); + TYPE2CODE.put(type, code); + CODE2TYPE.put(code, type); + } + } + + private TypeCodes() { } + + /** + * Returns {@code true} if the specified JSON String represents a value + * serialization that is prefixed with a type code. + * + * @param jsonString The JSON String representation of the value of a {@code PropertyState} + * @return {@code true} if the {@code jsonString} starts with a type + * code; {@code false} otherwise. + */ + public static boolean startsWithCode(String jsonString) { + return jsonString.length() >= 4 && jsonString.charAt(3) == ':'; + } + + /** + * Get the type code for the given property type. + * + * @param propertyType the property type + * @return the type code + */ + public static String getCodeForType(int propertyType) { + return TYPE2CODE.get(propertyType); + } + + /** + * Get the property type for the given type code. + * @param code the type code + * @return the property type. + */ + public static int getTypeForCode(String code) { + return CODE2TYPE.get(code); + } + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/AbstractNameMapper.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/AbstractNameMapper.java new file mode 100644 index 00000000000..978c108b157 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/AbstractNameMapper.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.namepath; + +public abstract class AbstractNameMapper implements NameMapper { + + protected abstract String getJcrPrefix(String oakPrefix); + + protected abstract String getOakPrefix(String jcrPrefix); + + protected abstract String getOakPrefixFromURI(String uri); + + public abstract boolean hasSessionLocalMappings(); + + @Override + public String getOakName(String jcrName) { + if (jcrName == null || jcrName.isEmpty()) { + return jcrName; + } + int pos = jcrName.indexOf(':'); + if (pos < 0) { + // no colon + return jcrName.startsWith("{}") ? jcrName.substring(2) : jcrName; + } else if (pos == 0) { + // Internal name, should not be visible to JCR clients + return null; + } else { + if (jcrName.charAt(0) == '{') { + int endpos = jcrName.indexOf('}'); + if (endpos > pos) { + // expanded name + + String nsuri = jcrName.substring(1, endpos); + String name = jcrName.substring(endpos + 1); + + String oakPref = getOakPrefixFromURI(nsuri); + if (oakPref == null) { + return null; + } else { + return oakPref + ':' + name; + } + } + } + + // otherwise: not an expanded name + + if (!hasSessionLocalMappings()) { + return jcrName; + } else { + String pref = jcrName.substring(0, pos); + String name = jcrName.substring(pos + 1); + String oakPrefix = getOakPrefix(pref); + if (oakPrefix == null) { + return null; // not a mapped name + } else { + return oakPrefix + ':' + name; + } + } + } + } + + @Override + public String getJcrName(String oakName) { + if (oakName == null || oakName.isEmpty()) { + return oakName; + } + + int pos = oakName.indexOf(':'); + if (pos < 0) { + // non-prefixed + return oakName; + } else if (pos == 0) { + // Internal name, should not be visible to JCR clients + throw new IllegalStateException("internal Oak name: " + oakName); + } else if (!hasSessionLocalMappings()) { + return oakName; + } else { + String pref = oakName.substring(0, pos); + String name = oakName.substring(pos + 1); + + if (pref.startsWith("{")) { + throw new IllegalStateException( + "invalid oak name (maybe expanded name leaked out?): " + + oakName); + } + + String jcrPrefix = getJcrPrefix(pref); + if (jcrPrefix == null) { + throw new IllegalStateException("invalid oak name: " + oakName); + } else { + return jcrPrefix + ':' + name; + } + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/JcrNameParser.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/JcrNameParser.java new file mode 100644 index 00000000000..a0e6afb8b55 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/JcrNameParser.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.namepath; + +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.util.XMLChar; + +/** + * Parses and validates JCR names. Upon successful completion of + * {@link #parse(String, Listener, int)} + * the specified listener is informed about the (resulting) JCR name. + * In case of failure {@link Listener#error(String)} is called indicating + * the reason. + */ +public class JcrNameParser { + + // constants for parser + private static final int STATE_PREFIX_START = 0; + private static final int STATE_PREFIX = 1; + private static final int STATE_NAME_START = 2; + private static final int STATE_NAME = 3; + private static final int STATE_URI_START = 4; + private static final int STATE_URI = 5; + + /** + * Listener interface for this name parser. + */ + interface Listener { + + /** + * Informs this listener that parsing the jcr name failed. + * + * @param message Details about the error. + * @see JcrNameParser#parse(String, Listener, int) + */ + void error(String message); + + /** + * Informs this listener about the result of + * {@link JcrNameParser#parse(String, Listener, int)} + * + * @param name The resulting name upon successful completion of + * {@link org.apache.jackrabbit.oak.namepath.JcrNameParser#parse(String, Listener, int)} + * @param index the index (or {@code 0} when not specified) + */ + boolean name(String name, int index); + } + + /** + * Avoid instantiation + */ + private JcrNameParser() { + } + + /** + * Parse the specified jcr name and inform the specified {@code listener} + * about the result or any error that may occur during parsing. + * + * @param jcrName The jcr name to be parsed. + * @param listener The listener to be informed about success or failure. + * @param index index, or {@code 0} when not specified + * @return whether parsing was successful + */ + public static boolean parse(String jcrName, Listener listener, int index) { + // trivial check + int len = jcrName == null ? 0 : jcrName.length(); + if (len == 0) { + listener.error("Empty name"); + return false; + } + if (".".equals(jcrName) || "..".equals(jcrName)) { + listener.error("Illegal name:" + jcrName); + return false; + } + + // parse the name + String prefix; + int nameStart = 0; + int state = STATE_PREFIX_START; + boolean trailingSpaces = false; + + for (int i = 0; i < len; i++) { + char c = jcrName.charAt(i); + if (c == ':') { + if (state == STATE_PREFIX_START) { + listener.error("Prefix must not be empty"); + return false; + } else if (state == STATE_PREFIX) { + if (trailingSpaces) { + listener.error("Trailing spaces not allowed"); + return false; + } + prefix = jcrName.substring(0, i); + if (!XMLChar.isValidNCName(prefix)) { + listener.error("Invalid name prefix: "+ prefix); + return false; + } + state = STATE_NAME_START; + } else if (state == STATE_URI) { + // ignore -> validation of uri later on. + } else { + listener.error("'" + c + "' not allowed in name"); + return false; + } + trailingSpaces = false; + } else if (c == ' ') { + if (state == STATE_PREFIX_START || state == STATE_NAME_START) { + listener.error("'" + c + "' not valid name start"); + return false; + } + trailingSpaces = true; + } else if (Character.isWhitespace(c) || c == '[' || c == ']' || c == '*' || c == '|') { + listener.error("'" + c + "' not allowed in name"); + return false; + } else if (c == '/') { + if (state == STATE_URI_START) { + state = STATE_URI; + } else if (state != STATE_URI) { + listener.error("'" + c + "' not allowed in name"); + return false; + } + trailingSpaces = false; + } else if (c == '{') { + if (state == STATE_PREFIX_START) { + state = STATE_URI_START; + } else if (state == STATE_URI_START || state == STATE_URI) { + // second '{' in the uri-part -> no valid expanded jcr-name. + // therefore reset the nameStart and change state. + state = STATE_NAME; + nameStart = 0; + } else if (state == STATE_NAME_START) { + state = STATE_NAME; + nameStart = i; + } + trailingSpaces = false; + } else if (c == '}') { + if (state == STATE_URI_START || state == STATE_URI) { + String tmp = jcrName.substring(1, i); + if (tmp.isEmpty() || tmp.indexOf(':') != -1) { + // The leading "{...}" part is empty or contains + // a colon, so we treat it as a valid namespace URI. + // More detailed validity checks (is it well formed, + // registered, etc.) are not needed here. + state = STATE_NAME_START; + } else if (tmp.equals("internal")) { + // As a special Jackrabbit backwards compatibility + // feature, support {internal} as a valid URI prefix + state = STATE_NAME_START; + } else if (tmp.indexOf('/') == -1) { + // The leading "{...}" contains neither a colon nor + // a slash, so we can interpret it as a a part of a + // normal local name. + state = STATE_NAME; + nameStart = 0; + } else { + listener.error("The URI prefix of the name " + jcrName + " is " + + "neither a valid URI nor a valid part of a local name."); + return false; + } + } else if (state == STATE_PREFIX_START) { + state = STATE_PREFIX; // prefix start -> validation later on will fail. + } else if (state == STATE_NAME_START) { + state = STATE_NAME; + nameStart = i; + } + trailingSpaces = false; + } else { + if (state == STATE_PREFIX_START) { + state = STATE_PREFIX; // prefix start + } else if (state == STATE_NAME_START) { + state = STATE_NAME; + nameStart = i; + } else if (state == STATE_URI_START) { + state = STATE_URI; + } + trailingSpaces = false; + } + } + + // take care of qualified jcrNames starting with '{' that are not having + // a terminating '}' -> make sure there are no illegal characters present. + if (state == STATE_URI && (jcrName.indexOf(':') > -1 || jcrName.indexOf('/') > -1)) { + listener.error("Local name may not contain ':' nor '/'"); + return false; + } + + if (nameStart == len || state == STATE_NAME_START) { + listener.error("Local name must not be empty"); + return false; + } + if (trailingSpaces) { + listener.error("Trailing spaces not allowed"); + return false; + } + + return listener.name(jcrName, index); + } + + public static boolean validate(String jcrName) { + Listener listener = new Listener() { + @Override + public void error(String message) { + } + + @Override + public boolean name(String name, int index) { + return true; + } + }; + return parse(jcrName, listener, 0); + } + + public static void checkName(String jcrName, boolean allowResidual) throws ConstraintViolationException { + if (jcrName == null || !(allowResidual && "*".equals(jcrName) || validate(jcrName))) { + throw new ConstraintViolationException("Not a valid JCR name '" + jcrName + '\''); + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/JcrPathParser.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/JcrPathParser.java new file mode 100644 index 00000000000..2dc5bee9872 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/JcrPathParser.java @@ -0,0 +1,298 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.namepath; + +public class JcrPathParser { + + // constants for parser + private static final int STATE_PREFIX_START = 0; + private static final int STATE_PREFIX = 1; + private static final int STATE_NAME_START = 2; + private static final int STATE_NAME = 3; + private static final int STATE_INDEX = 4; + private static final int STATE_INDEX_END = 5; + private static final int STATE_DOT = 6; + private static final int STATE_DOTDOT = 7; + private static final int STATE_URI = 8; + private static final int STATE_URI_END = 9; + + private static final char EOF = (char) -1; + + private JcrPathParser() { + } + + interface Listener extends JcrNameParser.Listener { + boolean root(); + boolean current(); + boolean parent(); + } + + public static boolean parse(String jcrPath, Listener listener) { + // check for length + int len = jcrPath == null ? 0 : jcrPath.length(); + + // shortcut for root path + if (len == 1 && jcrPath.charAt(0) == '/') { + listener.root(); + return true; + } + + // short cut for empty path + if (len == 0) { + return true; + } + + // check if absolute path + int pos = 0; + if (jcrPath.charAt(0) == '/') { + if (!listener.root()) { + return false; + } + pos++; + } + + // parse the path + int state = STATE_PREFIX_START; + + int lastPos = pos; + String name = null; + + int index = 0; + boolean wasSlash = false; + + while (pos <= len) { + char c = pos == len ? EOF : jcrPath.charAt(pos); + pos++; + // special check for whitespace + if (c != ' ' && Character.isWhitespace(c)) { + c = '\t'; + } + + switch (c) { + case '/': + case EOF: + if (state == STATE_PREFIX_START && c != EOF) { + listener.error('\'' + jcrPath + "' is not a valid path. " + + "double slash '//' not allowed."); + return false; + } + if (state == STATE_PREFIX + || state == STATE_NAME + || state == STATE_INDEX_END + || state == STATE_URI_END) { + + // eof path element + if (name == null) { + if (wasSlash) { + listener.error('\'' + jcrPath + "' is not a valid path: " + + "Trailing slashes not allowed in prefixes and names."); + return false; + } + name = jcrPath.substring(lastPos, pos - 1); + } + + if (!JcrNameParser.parse(name, listener, index)) { + return false; + } + state = STATE_PREFIX_START; + lastPos = pos; + name = null; + index = 0; + } else if (state == STATE_DOT) { + if (!listener.current()) { + return false; + } + lastPos = pos; + state = STATE_PREFIX_START; + } else if (state == STATE_DOTDOT) { + if (!listener.parent()) { + return false; + } + lastPos = pos; + state = STATE_PREFIX_START; + } else if (state != STATE_URI + && !(state == STATE_PREFIX_START && c == EOF)) { // ignore trailing slash + listener.error('\'' + jcrPath + "' is not a valid path. '" + c + + "' not a valid name character."); + return false; + } + break; + + case '.': + if (state == STATE_PREFIX_START) { + state = STATE_DOT; + } else if (state == STATE_DOT) { + state = STATE_DOTDOT; + } else if (state == STATE_DOTDOT) { + state = STATE_PREFIX; + } else if (state == STATE_INDEX_END) { + listener.error('\'' + jcrPath + "' is not a valid path. '" + c + + "' not valid after index. '/' expected."); + return false; + } + break; + + case ':': + if (state == STATE_PREFIX_START) { + listener.error('\'' + jcrPath + "' is not a valid path. Prefix " + + "must not be empty"); + return false; + } else if (state == STATE_PREFIX) { + if (wasSlash) { + listener.error('\'' + jcrPath + "' is not a valid path: " + + "Trailing slashes not allowed in prefixes and names."); + return false; + } + state = STATE_NAME_START; + // don't reset the lastPos/pos since prefix+name are passed together to the NameResolver + } else if (state != STATE_URI) { + listener.error('\'' + jcrPath + "' is not a valid path. '" + c + + "' not valid name character"); + return false; + } + break; + + case '[': + if (state == STATE_PREFIX || state == STATE_NAME) { + if (wasSlash) { + listener.error('\'' + jcrPath + "' is not a valid path: " + + "Trailing slashes not allowed in prefixes and names."); + return false; + } + state = STATE_INDEX; + name = jcrPath.substring(lastPos, pos - 1); + lastPos = pos; + } + break; + + case ']': + if (state == STATE_INDEX) { + try { + index = Integer.parseInt(jcrPath.substring(lastPos, pos - 1)); + } catch (NumberFormatException e) { + listener.error('\'' + jcrPath + "' is not a valid path. " + + "NumberFormatException in index: " + + jcrPath.substring(lastPos, pos - 1)); + return false; + } + if (index < 0) { + listener.error('\'' + jcrPath + "' is not a valid path. " + + "Index number invalid: " + index); + return false; + } + state = STATE_INDEX_END; + } else { + listener.error('\'' + jcrPath + "' is not a valid path. '" + c + + "' not a valid name character."); + return false; + } + break; + + case ' ': + if (state == STATE_PREFIX_START || state == STATE_NAME_START) { + listener.error('\'' + jcrPath + "' is not a valid path. '" + c + + "' not valid name start"); + return false; + } else if (state == STATE_INDEX_END) { + listener.error('\'' + jcrPath + "' is not a valid path. '" + c + + "' not valid after index. '/' expected."); + return false; + } else if (state == STATE_DOT || state == STATE_DOTDOT) { + state = STATE_PREFIX; + } + break; + + case '\t': + listener.error('\'' + jcrPath + "' is not a valid path. " + + "Whitespace not a allowed in name."); + return false; + case '*': + case '|': + listener.error('\'' + jcrPath + "' is not a valid path. '" + c + + "' not a valid name character."); + return false; + case '{': + if (state == STATE_PREFIX_START && lastPos == pos-1) { + // '{' marks the start of a uri enclosed in an expanded name + // instead of the usual namespace prefix, if it is + // located at the beginning of a new segment. + state = STATE_URI; + } else if (state == STATE_NAME_START || state == STATE_DOT || state == STATE_DOTDOT) { + // otherwise it's part of the local name + state = STATE_NAME; + } + break; + + case '}': + if (state == STATE_URI) { + state = STATE_URI_END; + } + break; + + default: + if (state == STATE_PREFIX_START || state == STATE_DOT || state == STATE_DOTDOT) { + state = STATE_PREFIX; + } else if (state == STATE_NAME_START) { + state = STATE_NAME; + } else if (state == STATE_INDEX_END) { + listener.error('\'' + jcrPath + "' is not a valid path. '" + c + + "' not valid after index. '/' expected."); + return false; + } + } + wasSlash = c == ' '; + } + return true; + } + + public static boolean validate(String jcrPath) { + Listener listener = new Listener() { + boolean hasRoot; + @Override + public boolean root() { + if (hasRoot) { + return false; + } + else { + hasRoot = true; + return true; + } + } + + @Override + public boolean current() { + return true; + } + + @Override + public boolean parent() { + return true; + } + + @Override + public void error(String message) { + } + + @Override + public boolean name(String name, int index) { + return true; + } + + }; + return parse(jcrPath, listener); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/NameMapper.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/NameMapper.java new file mode 100644 index 00000000000..fbf87bd84a9 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/NameMapper.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.namepath; + +import javax.annotation.CheckForNull; + +public interface NameMapper { + + /** + * Returns the Oak name for the given JCR name, or {@code null} if no + * such mapping exists because the given JCR name contains an unknown + * namespace URI or prefix, or is otherwise invalid. + * + * @param jcrName JCR name + * @return Oak name, or {@code null} + */ + @CheckForNull + String getOakName(String jcrName); + + /** + * Returns whether the mapper has prefix remappings; when there aren't + * any, prefixed names do not need to be converted at all + * + * @return {@code true} if prefixes have been remapped + */ + boolean hasSessionLocalMappings(); + + /** + * Returns the JCR name for the given Oak name. The given name is + * expected to have come from a valid Oak repository that contains + * only valid names with proper namespace mappings. If that's not + * the case, either a programming error or a repository corruption + * has occurred and an appropriate unchecked exception gets thrown. + * + * @param oakName Oak name + * @return JCR name + */ + @CheckForNull + String getJcrName(String oakName); +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/NameMapperImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/NameMapperImpl.java new file mode 100644 index 00000000000..233bdaaa90e --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/NameMapperImpl.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.namepath; + +import javax.annotation.CheckForNull; +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.plugins.name.ReadOnlyNamespaceRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NameMapperImpl extends AbstractNameMapper { + private static final Logger log = LoggerFactory.getLogger(NameMapperImpl.class); + + private final Tree root; + private final NamespaceRegistry nsReg = new ReadOnlyNamespaceRegistry() { + @Override + protected Tree getReadTree() { + return root; + } + }; + + public NameMapperImpl(Tree root) { + this.root = root; + } + + @Override + @CheckForNull + protected String getJcrPrefix(String oakPrefix) { + return oakPrefix; + } + + @Override + @CheckForNull + protected String getOakPrefix(String jcrPrefix) { + return jcrPrefix; + } + + @Override + @CheckForNull + protected String getOakPrefixFromURI(String uri) { + try { + return nsReg.getPrefix(uri); + } catch (RepositoryException e) { + log.debug("Could not get OAK prefix for URI " + uri); + return null; + } + } + + @Override + public boolean hasSessionLocalMappings() { + return false; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/NamePathMapper.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/NamePathMapper.java new file mode 100644 index 00000000000..fc696d41c3c --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/NamePathMapper.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.namepath; + +import javax.annotation.Nonnull; + +/** + * The {@code NamePathMapper} interface combines {@code NameMapper} and + * {@code PathMapper}. + */ +public interface NamePathMapper extends NameMapper, PathMapper { + + public NamePathMapper DEFAULT = new Default(); + + /** + * Default implementation that doesn't perform any conversions for cases + * where a mapper object only deals with oak internal names and paths. + */ + public class Default implements NamePathMapper { + + @Override + public String getOakName(String jcrName) { + return jcrName; + } + + @Override + public boolean hasSessionLocalMappings() { + return false; + } + + @Override + public String getJcrName(String oakName) { + return oakName; + } + + @Override + public String getOakPath(String jcrPath) { + return jcrPath; + } + + @Override + public String getOakPathKeepIndex(String jcrPath) { + return jcrPath; + } + + @Nonnull + @Override + public String getJcrPath(String oakPath) { + return oakPath; + } + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/NamePathMapperImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/NamePathMapperImpl.java new file mode 100644 index 00000000000..65639f1dcb0 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/NamePathMapperImpl.java @@ -0,0 +1,301 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.namepath; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * NamePathMapperImpl... + */ +public class NamePathMapperImpl implements NamePathMapper { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(NamePathMapperImpl.class); + + private final NameMapper nameMapper; + private final IdentifierManager idManager; + + public NamePathMapperImpl(NameMapper nameMapper) { + this.nameMapper = nameMapper; + this.idManager = null; + } + + public NamePathMapperImpl(NameMapper nameMapper, IdentifierManager idManager) { + this.nameMapper = nameMapper; + this.idManager = idManager; + } + + //---------------------------------------------------------< NameMapper >--- + @Override + public String getOakName(String jcrName) { + return nameMapper.getOakName(jcrName); + } + + @Override + public String getJcrName(String oakName) { + return nameMapper.getJcrName(oakName); + } + + @Override + public boolean hasSessionLocalMappings() { + return nameMapper.hasSessionLocalMappings(); + } + + //---------------------------------------------------------< PathMapper >--- + @Override + public String getOakPath(String jcrPath) { + return getOakPath(jcrPath, false); + } + + @Override + public String getOakPathKeepIndex(String jcrPath) { + return getOakPath(jcrPath, true); + } + + @Override + @Nonnull + public String getJcrPath(String oakPath) { + final List elements = new ArrayList(); + + if ("/".equals(oakPath)) { + // avoid the need to special case the root path later on + return "/"; + } + + JcrPathParser.Listener listener = new JcrPathParser.Listener() { + @Override + public boolean root() { + if (!elements.isEmpty()) { + throw new IllegalArgumentException("/ on non-empty path"); + } + elements.add(""); + return true; + } + + @Override + public boolean current() { + // nothing to do here + return false; + } + + @Override + public boolean parent() { + if (elements.isEmpty() || "..".equals(elements.get(elements.size() - 1))) { + elements.add(".."); + return true; + } + elements.remove(elements.size() - 1); + return true; + } + + @Override + public void error(String message) { + throw new IllegalArgumentException(message); + } + + @Override + public boolean name(String name, int index) { + if (index > 1) { + throw new IllegalArgumentException("index > 1"); + } + String p = nameMapper.getJcrName(name); + elements.add(p); + return true; + } + }; + + JcrPathParser.parse(oakPath, listener); + + // empty path: map to "." + if (elements.isEmpty()) { + return "."; + } + + StringBuilder jcrPath = new StringBuilder(); + for (String element : elements) { + if (element.isEmpty()) { + // root + jcrPath.append('/'); + } + else { + jcrPath.append(element); + jcrPath.append('/'); + } + } + + jcrPath.deleteCharAt(jcrPath.length() - 1); + return jcrPath.toString(); + } + + private String getOakPath(String jcrPath, final boolean keepIndex) { + if ("/".equals(jcrPath)) { + // avoid the need to special case the root path later on + return "/"; + } + + int length = jcrPath.length(); + + // identifier path? + if (length > 0 && jcrPath.charAt(0) == '[') { + if (jcrPath.charAt(length - 1) != ']') { + // TODO error handling? + log.debug("Could not parse path " + jcrPath + ": unterminated identifier"); + return null; + } + if (this.idManager == null) { + // TODO error handling? + log.debug("Could not parse path " + jcrPath + ": could not resolve identifier"); + return null; + } + return this.idManager.getPath(jcrPath.substring(1, length - 1)); + } + + boolean hasClarkBrackets = false; + boolean hasIndexBrackets = false; + boolean hasColon = false; + boolean hasNameStartingWithDot = false; + boolean hasTrailingSlash = false; + + char prev = 0; + for (int i = 0; i < length; i++) { + char c = jcrPath.charAt(i); + + if (c == '{' || c == '}') { + hasClarkBrackets = true; + } else if (c == '[' || c == ']') { + hasIndexBrackets = true; + } else if (c == ':') { + hasColon = true; + } else if (c == '.' && (prev == 0 || prev == '/')) { + hasNameStartingWithDot = true; + } else if(c == '/' && i == (length - 1)){ + hasTrailingSlash = true; + } + + prev = c; + } + + // try a shortcut + if (!hasNameStartingWithDot && !hasClarkBrackets && !hasIndexBrackets) { + if (!hasColon || !hasSessionLocalMappings()) { + if (JcrPathParser.validate(jcrPath)) { + if(hasTrailingSlash){ + return jcrPath.substring(0, length - 1); + } + return jcrPath; + } + else { + log.debug("Invalid path: {}", jcrPath); + return null; + } + } + } + + final List elements = new ArrayList(); + final StringBuilder parseErrors = new StringBuilder(); + + JcrPathParser.Listener listener = new JcrPathParser.Listener() { + + @Override + public boolean root() { + if (!elements.isEmpty()) { + parseErrors.append("/ on non-empty path"); + return false; + } + elements.add(""); + return true; + } + + @Override + public boolean current() { + // nothing to do here + return true; + } + + @Override + public boolean parent() { + if (elements.isEmpty() || "..".equals(elements.get(elements.size() - 1))) { + elements.add(".."); + return true; + } + elements.remove(elements.size() - 1); + return true; + } + + @Override + public void error(String message) { + parseErrors.append(message); + } + + @Override + public boolean name(String name, int index) { + if (!keepIndex && index > 1) { + parseErrors.append("index > 1"); + return false; + } + String p = nameMapper.getOakName(name); + if (p == null) { + parseErrors.append("Invalid name: ").append(name); + return false; + } + if (keepIndex && index > 0) { + p += "[" + index + ']'; + } + elements.add(p); + return true; + } + }; + + JcrPathParser.parse(jcrPath, listener); + if (parseErrors.length() != 0) { + log.debug("Could not parse path " + jcrPath + ": " + parseErrors.toString()); + return null; + } + + // Empty path maps to "" + if (elements.isEmpty()) { + return ""; + } + + StringBuilder oakPath = new StringBuilder(); + for (String element : elements) { + if (element.isEmpty()) { + // root + oakPath.append('/'); + } + else { + oakPath.append(element); + oakPath.append('/'); + } + } + + // root path is special-cased early on so it does not need to + // be considered here + oakPath.deleteCharAt(oakPath.length() - 1); + return oakPath.toString(); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/PathMapper.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/PathMapper.java new file mode 100644 index 00000000000..fd2172a1b9e --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/namepath/PathMapper.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.namepath; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +/** + * {@code PathMapper} instances provide methods for mapping paths from their JCR + * string representation to their Oak representation and vice versa. + * + * The Oak representation of a path consists of a forward slash followed by the + * names of the respective items in the {@link org.apache.jackrabbit.oak.api.Tree} + * separated by forward slashes. + */ +public interface PathMapper { + + /** + * Returns the Oak path for the given JCR path, or {@code null} if no + * such mapping exists because the given JCR path contains a name element + * with an unknown namespace URI or prefix, or is otherwise invalid. + * + * @param jcrPath JCR path + * @return Oak path, or {@code null} + */ + @CheckForNull + String getOakPath(String jcrPath); + + /** + * As {@link #getOakPath(String)}, but preserving the index information + * + * @param jcrPath JCR path + * @return mapped path, or {@code null} + */ + @CheckForNull + String getOakPathKeepIndex(String jcrPath); + + /** + * Returns the JCR path for the given Oak path. The given path is + * expected to have come from a valid Oak repository that contains + * only valid paths whose name elements only use proper namespace + * mappings. If that's not the case, either a programming error or + * a repository corruption has occurred and an appropriate unchecked + * exception gets thrown. + * + * @param oakPath Oak path + * @return JCR path + */ + @Nonnull + String getJcrPath(String oakPath); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/AbstractServiceTracker.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/AbstractServiceTracker.java new file mode 100644 index 00000000000..c2c0cc91e75 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/AbstractServiceTracker.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.osgi; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +/** + * AbstractServiceTracker is a base class for the various OSGi based + * providers. + */ +public abstract class AbstractServiceTracker implements ServiceTrackerCustomizer { + + private BundleContext context; + + private ServiceTracker tracker; + + private final Map services = + new HashMap(); + + private final Class serviceClass; + + public AbstractServiceTracker(Class serviceClass) { + this.serviceClass = serviceClass; + } + + public void start(BundleContext bundleContext) throws Exception { + context = bundleContext; + tracker = new ServiceTracker( + bundleContext, serviceClass.getName(), this); + tracker.open(); + } + + public void stop() throws Exception { + tracker.close(); + } + + /** + * Returns all services of type T currently available. + * + * @return services currently available. + */ + protected List getServices() { + synchronized (this) { + return new ArrayList(services.values()); + } + } + + //------------------------< ServiceTrackerCustomizer >---------------------- + + @Override + public Object addingService(ServiceReference reference) { + Object service = context.getService(reference); + + if (serviceClass.isInstance(service)) { + synchronized (this) { + services.put(reference, (T) service); + } + return service; + } else { + context.ungetService(reference); + return null; + } + } + + @Override + public void modifiedService(ServiceReference reference, Object service) { + // nothing to do + } + + @Override + public void removedService(ServiceReference reference, Object service) { + synchronized (this) { + services.remove(reference); + } + context.ungetService(reference); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/Activator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/Activator.java new file mode 100644 index 00000000000..1150435e09a --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/Activator.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.osgi; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.kernel.KernelNodeStore; +import org.apache.jackrabbit.oak.plugins.index.IndexHookManager; +import org.apache.jackrabbit.oak.plugins.nodetype.DefaultTypeEditor; +import org.apache.jackrabbit.oak.spi.commit.CompositeHook; +import org.apache.jackrabbit.oak.spi.commit.ValidatingHook; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +public class Activator implements BundleActivator, ServiceTrackerCustomizer { + + private BundleContext context; + + private ServiceTracker tracker; + + private final OsgiIndexProvider indexProvider = new OsgiIndexProvider(); + + private final OsgiIndexHookProvider indexHookProvider = new OsgiIndexHookProvider(); + + private final OsgiValidatorProvider validatorProvider = new OsgiValidatorProvider(); + + private final OsgiRepositoryInitializer kernelTracker = new OsgiRepositoryInitializer(); + + private final Map services = + new HashMap(); + + //----------------------------------------------------< BundleActivator >--- + + @Override + public void start(BundleContext bundleContext) throws Exception { + context = bundleContext; + + indexProvider.start(bundleContext); + indexHookProvider.start(bundleContext); + validatorProvider.start(bundleContext); + kernelTracker.start(bundleContext); + + tracker = new ServiceTracker( + context, MicroKernel.class.getName(), this); + tracker.open(); + } + + @Override + public void stop(BundleContext bundleContext) throws Exception { + tracker.close(); + + indexProvider.stop(); + indexHookProvider.stop(); + validatorProvider.stop(); + kernelTracker.stop(); + } + + //-------------------------------------------< ServiceTrackerCustomizer >--- + + @Override + public Object addingService(ServiceReference reference) { + Object service = context.getService(reference); + if (service instanceof MicroKernel) { + MicroKernel kernel = (MicroKernel) service; + kernelTracker.initialize(new KernelNodeStore(kernel)); + Oak oak = new Oak(kernel) + .with(new CompositeHook( + // TODO: DefaultTypeEditor is JCR specific and does not belong here + new DefaultTypeEditor(), + new ValidatingHook(validatorProvider), + new IndexHookManager(indexHookProvider))) + .with(indexProvider); + services.put(reference, context.registerService( + ContentRepository.class.getName(), + oak.createContentRepository(), + new Properties())); + return service; + } else { + context.ungetService(reference); + return null; + } + } + + @Override + public void modifiedService(ServiceReference reference, Object service) { + // nothing to do + } + + @Override + public void removedService(ServiceReference reference, Object service) { + ServiceRegistration registration = services.remove(reference); + registration.unregister(); + context.ungetService(reference); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiIndexHookProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiIndexHookProvider.java new file mode 100644 index 00000000000..0ec22822046 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiIndexHookProvider.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.osgi; + +import java.util.List; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.plugins.index.CompositeIndexHookProvider; +import org.apache.jackrabbit.oak.plugins.index.IndexHook; +import org.apache.jackrabbit.oak.plugins.index.IndexHookProvider; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; + +/** + * This IndexHook provider combines all index hooks of all available OSGi + * IndexHook providers. + */ +public class OsgiIndexHookProvider extends + AbstractServiceTracker implements IndexHookProvider { + + public OsgiIndexHookProvider() { + super(IndexHookProvider.class); + } + + @Override @Nonnull + public List getIndexHooks(String type, + NodeBuilder builder) { + IndexHookProvider composite = CompositeIndexHookProvider + .compose(getServices()); + return composite.getIndexHooks(type, builder); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiIndexProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiIndexProvider.java new file mode 100644 index 00000000000..4970544ee19 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiIndexProvider.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.osgi; + +import java.util.List; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.spi.query.CompositeQueryIndexProvider; +import org.apache.jackrabbit.oak.spi.query.QueryIndex; +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * This index provider combines all indexes of all available OSGi index + * providers. + */ +public class OsgiIndexProvider + extends AbstractServiceTracker + implements QueryIndexProvider { + + public OsgiIndexProvider() { + super(QueryIndexProvider.class); + } + + @Override @Nonnull + public List getQueryIndexes(NodeState nodeState) { + QueryIndexProvider composite = + CompositeQueryIndexProvider.compose(getServices()); + return composite.getQueryIndexes(nodeState); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiRepositoryInitializer.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiRepositoryInitializer.java new file mode 100644 index 00000000000..7e9e94a5ba7 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiRepositoryInitializer.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.osgi; + +import org.apache.jackrabbit.oak.spi.lifecycle.RepositoryInitializer; +import org.apache.jackrabbit.oak.spi.state.NodeStore; +import org.osgi.framework.ServiceReference; + +/** + * Implements a service tracker that keeps track of all + * {@link RepositoryInitializer}s in the system and calls the available + * method once the micro kernel is available. + */ +public class OsgiRepositoryInitializer + extends AbstractServiceTracker + implements RepositoryInitializer { + + /** + * The reference to the micro kernel once available. + */ + private volatile NodeStore store; + + public OsgiRepositoryInitializer() { + super(RepositoryInitializer.class); + } + + @Override + public void initialize(NodeStore store) { + this.store = store; + if (store != null) { + for (RepositoryInitializer mki : getServices()) { + mki.initialize(store); + } + } + } + + @Override + public Object addingService(ServiceReference reference) { + RepositoryInitializer mki = + (RepositoryInitializer) super.addingService(reference); + NodeStore store = this.store; + if (store != null) { + mki.initialize(store); + } + return mki; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiValidatorProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiValidatorProvider.java new file mode 100644 index 00000000000..b4323a9c863 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiValidatorProvider.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.osgi; + +import org.apache.jackrabbit.oak.spi.commit.CompositeValidatorProvider; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * This validator provider combines all validators of all available OSGi validator + * providers. + */ +public class OsgiValidatorProvider + extends AbstractServiceTracker + implements ValidatorProvider { + + public OsgiValidatorProvider() { + super(ValidatorProvider.class); + } + + //------------------------------------------------------------< ValidatorProvider >--- + + @Override + public Validator getRootValidator(NodeState before, NodeState after) { + return CompositeValidatorProvider.compose(getServices()) + .getRootValidator(before, after); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/commit/AnnotatingConflictHandler.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/commit/AnnotatingConflictHandler.java new file mode 100644 index 00000000000..2e6d7394ef0 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/commit/AnnotatingConflictHandler.java @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.commit; + +import java.util.List; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.commit.ConflictHandler; +import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import com.google.common.collect.Lists; + +import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES; +import static org.apache.jackrabbit.oak.api.Type.NAMES; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.ADD_EXISTING; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.CHANGE_CHANGED; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.CHANGE_DELETED; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.DELETE_CHANGED; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.DELETE_DELETED; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.MIX_REP_MERGE_CONFLICT; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.REP_OURS; + +/** + * This {@link ConflictHandler} implementation resolves conflicts to + * {@link Resolution#THEIRS} and in addition marks nodes where a conflict + * occurred with the mixin {@code rep:MergeConflict}: + * + *
+ * [rep:MergeConflict]
+ *   mixin
+ *   primaryitem rep:ours
+ *   + rep:ours (nt:unstructured) protected IGNORE
+ * 
+ * + * The {@code rep:ours} sub node contains our version of the node prior to + * the conflict. + * + * @see ConflictValidator + */ +public class AnnotatingConflictHandler implements ConflictHandler { + + @Override + public Resolution addExistingProperty(NodeBuilder parent, PropertyState ours, PropertyState theirs) { + NodeBuilder marker = addConflictMarker(parent); + marker.child(ADD_EXISTING).setProperty(ours); + return Resolution.THEIRS; + } + + @Override + public Resolution changeDeletedProperty(NodeBuilder parent, PropertyState ours) { + NodeBuilder marker = addConflictMarker(parent); + marker.child(CHANGE_DELETED).setProperty(ours); + return Resolution.THEIRS; + } + + @Override + public Resolution changeChangedProperty(NodeBuilder parent, PropertyState ours, PropertyState theirs) { + NodeBuilder marker = addConflictMarker(parent); + marker.child(CHANGE_CHANGED).setProperty(ours); + return Resolution.THEIRS; + } + + @Override + public Resolution deleteChangedProperty(NodeBuilder parent, PropertyState theirs) { + NodeBuilder marker = addConflictMarker(parent); + marker.child(DELETE_CHANGED).setProperty(theirs); + return Resolution.THEIRS; + } + + @Override + public Resolution deleteDeletedProperty(NodeBuilder parent, PropertyState ours) { + NodeBuilder marker = addConflictMarker(parent); + marker.child(DELETE_DELETED).setProperty(ours); + return Resolution.THEIRS; + } + + @Override + public Resolution addExistingNode(NodeBuilder parent, String name, NodeState ours, NodeState theirs) { + NodeBuilder marker = addConflictMarker(parent); + addChild(marker.child(ADD_EXISTING), name, ours); + return Resolution.THEIRS; + } + + @Override + public Resolution changeDeletedNode(NodeBuilder parent, String name, NodeState ours) { + NodeBuilder marker = addConflictMarker(parent); + addChild(marker.child(CHANGE_DELETED), name, ours); + return Resolution.THEIRS; + } + + @Override + public Resolution deleteChangedNode(NodeBuilder parent, String name, NodeState theirs) { + NodeBuilder marker = addConflictMarker(parent); + markChild(marker.child(DELETE_CHANGED), name); + return Resolution.THEIRS; + } + + @Override + public Resolution deleteDeletedNode(NodeBuilder parent, String name) { + NodeBuilder marker = addConflictMarker(parent); + markChild(marker.child(DELETE_DELETED), name); + return Resolution.THEIRS; + } + + private static NodeBuilder addConflictMarker(NodeBuilder parent) { + PropertyState jcrMixin = parent.getProperty(JCR_MIXINTYPES); + List mixins; + if (jcrMixin == null) { + mixins = Lists.newArrayList(); + } + else { + mixins = Lists.newArrayList(jcrMixin.getValue(NAMES)); + } + if (!mixins.contains(MIX_REP_MERGE_CONFLICT)) { + mixins.add(MIX_REP_MERGE_CONFLICT); + parent.setProperty(JCR_MIXINTYPES, mixins, NAMES); + } + + return parent.child(REP_OURS); + } + + private static void addChild(NodeBuilder parent, String name, NodeState state) { + NodeBuilder child = parent.child(name); + for (PropertyState property : state.getProperties()) { + child.setProperty(property); + } + for (ChildNodeEntry entry : state.getChildNodeEntries()) { + addChild(child, entry.getName(), entry.getNodeState()); + } + } + + private static void markChild(NodeBuilder parent, String name) { + parent.child(name); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/commit/ConflictHandlerWrapper.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/commit/ConflictHandlerWrapper.java new file mode 100644 index 00000000000..a28bae1ba50 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/commit/ConflictHandlerWrapper.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.commit; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.commit.ConflictHandler; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * This {@link ConflictHandler} implementation wraps another conflict handler + * and forwards all calls to the wrapped handler. Sub classes may override + * methods of this class and intercept calls they are interested in. + */ +public class ConflictHandlerWrapper implements ConflictHandler { + + protected final ConflictHandler handler; + + public ConflictHandlerWrapper(ConflictHandler handler) { + this.handler = handler; + } + + @Override + public Resolution addExistingProperty(NodeBuilder parent, + PropertyState ours, + PropertyState theirs) { + return handler.addExistingProperty(parent, ours, theirs); + } + + @Override + public Resolution changeDeletedProperty(NodeBuilder parent, + PropertyState ours) { + return handler.changeDeletedProperty(parent, ours); + } + + @Override + public Resolution changeChangedProperty(NodeBuilder parent, + PropertyState ours, + PropertyState theirs) { + return handler.changeChangedProperty(parent, ours, theirs); + } + + @Override + public Resolution deleteDeletedProperty(NodeBuilder parent, + PropertyState ours) { + return handler.deleteDeletedProperty(parent, ours); + } + + @Override + public Resolution deleteChangedProperty(NodeBuilder parent, + PropertyState theirs) { + return handler.deleteChangedProperty(parent, theirs); + } + + @Override + public Resolution addExistingNode(NodeBuilder parent, + String name, + NodeState ours, + NodeState theirs) { + return handler.addExistingNode(parent, name, ours, theirs); + } + + @Override + public Resolution changeDeletedNode(NodeBuilder parent, + String name, + NodeState ours) { + return handler.changeDeletedNode(parent, name, ours); + } + + @Override + public Resolution deleteChangedNode(NodeBuilder parent, + String name, + NodeState theirs) { + return handler.deleteChangedNode(parent, name, theirs); + } + + @Override + public Resolution deleteDeletedNode(NodeBuilder parent, String name) { + return handler.deleteDeletedNode(parent, name); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/commit/ConflictValidator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/commit/ConflictValidator.java new file mode 100644 index 00000000000..611a02efa31 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/commit/ConflictValidator.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.plugins.commit; + +import javax.jcr.InvalidItemStateException; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants; +import org.apache.jackrabbit.oak.spi.commit.DefaultValidator; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import static org.apache.jackrabbit.oak.api.Type.STRINGS; + +/** + * {@link Validator} which checks the presence of conflict markers + * in the tree in fails the commit if any are found. + * + * @see AnnotatingConflictHandler + */ +public class ConflictValidator extends DefaultValidator { + @Override + public void propertyAdded(PropertyState after) throws CommitFailedException { + failOnMergeConflict(after); + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) + throws CommitFailedException { + failOnMergeConflict(after); + } + + @Override + public Validator childNodeAdded(String name, NodeState after) { + return this; + } + + @Override + public Validator childNodeChanged(String name, NodeState before, NodeState after) { + return this; + } + + @Override + public Validator childNodeDeleted(String name, NodeState before) { + return this; + } + + private static void failOnMergeConflict(PropertyState property) throws CommitFailedException { + if (JcrConstants.JCR_MIXINTYPES.equals(property.getName())) { + assert property.isArray(); + for (String v : property.getValue(STRINGS)) { + if (NodeTypeConstants.MIX_REP_MERGE_CONFLICT.equals(v)) { + throw new CommitFailedException(new InvalidItemStateException("Item has unresolved conflicts")); + } + } + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/commit/ConflictValidatorProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/commit/ConflictValidatorProvider.java new file mode 100644 index 00000000000..c53d14723e9 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/commit/ConflictValidatorProvider.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.plugins.commit; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +@Component +@Service(ValidatorProvider.class) +public class ConflictValidatorProvider implements ValidatorProvider { + + @Override + public Validator getRootValidator(NodeState before, NodeState after) { + return new ConflictValidator(); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/commit/DefaultConflictHandler.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/commit/DefaultConflictHandler.java new file mode 100644 index 00000000000..48f2df3528d --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/commit/DefaultConflictHandler.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.plugins.commit; + +import org.apache.jackrabbit.oak.spi.commit.ConflictHandler; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * This implementation of a {@link ConflictHandler} always returns the same resolution. + * It can be used to implement default behaviour or as a base class for more specialised + * implementations. + */ +public class DefaultConflictHandler implements ConflictHandler { + + /** + * A {@code ConflictHandler} which always return {@link Resolution#OURS}. + */ + public static final ConflictHandler OURS = new DefaultConflictHandler(Resolution.OURS); + + /** + * A {@code ConflictHandler} which always return {@link Resolution#THEIRS}. + */ + public static final ConflictHandler THEIRS = new DefaultConflictHandler(Resolution.THEIRS); + + private final Resolution resolution; + + /** + * Create a new {@code ConflictHandler} which always returns {@code resolution}. + * + * @param resolution the resolution to return from all methods of this + * {@code ConflictHandler} instance. + */ + public DefaultConflictHandler(Resolution resolution) { + this.resolution = resolution; + } + + @Override + public Resolution addExistingProperty(NodeBuilder parent, PropertyState ours, PropertyState theirs) { + return resolution; + } + + @Override + public Resolution changeDeletedProperty(NodeBuilder parent, PropertyState ours) { + return resolution; + } + + @Override + public Resolution changeChangedProperty(NodeBuilder parent, PropertyState ours, PropertyState theirs) { + return resolution; + } + + @Override + public Resolution deleteChangedProperty(NodeBuilder parent, PropertyState theirs) { + return resolution; + } + + @Override + public Resolution deleteDeletedProperty(NodeBuilder parent, PropertyState ours) { + return resolution; + } + + @Override + public Resolution addExistingNode(NodeBuilder parent, String name, NodeState ours, NodeState theirs) { + return resolution; + } + + @Override + public Resolution changeDeletedNode(NodeBuilder parent, String name, NodeState ours) { + return resolution; + } + + @Override + public Resolution deleteChangedNode(NodeBuilder parent, String name, NodeState theirs) { + return resolution; + } + + @Override + public Resolution deleteDeletedNode(NodeBuilder parent, String name) { + return resolution; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/identifier/IdentifierManager.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/identifier/IdentifierManager.java new file mode 100644 index 00000000000..8755efcaa1e --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/identifier/IdentifierManager.java @@ -0,0 +1,309 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.identifier; + +import java.text.ParseException; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.jcr.PropertyType; +import javax.jcr.query.Query; + +import com.google.common.base.Charsets; +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Result; +import org.apache.jackrabbit.oak.api.ResultRow; +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.commons.PathUtils; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.plugins.memory.StringPropertyState; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.jackrabbit.oak.api.Type.STRING; +import static org.apache.jackrabbit.oak.api.Type.STRINGS; + +/** + * IdentifierManager... + */ +public class IdentifierManager { + + private static final Logger log = LoggerFactory.getLogger(IdentifierManager.class); + + private final Root root; + + public IdentifierManager(Root root) { + this.root = root; + } + + @Nonnull + public static String generateUUID() { + return UUID.randomUUID().toString(); + } + + @Nonnull + public static String generateUUID(String hint) { + UUID uuid = UUID.nameUUIDFromBytes(hint.getBytes(Charsets.UTF_8)); + return uuid.toString(); + } + + public static boolean isValidUUID(String uuid) { + try { + UUID.fromString(uuid); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + /** + * + * @param uuid + * @throws IllegalArgumentException If the specified uuid has an invalid format. + */ + public static void checkUUIDFormat(String uuid) throws IllegalArgumentException { + UUID.fromString(uuid); + } + + @Nonnull + public String getIdentifier(Tree tree) { + PropertyState property = tree.getProperty(JcrConstants.JCR_UUID); + if (property == null) { + // TODO calculate the identifier from closest referenceable parent + // TODO and a relative path irrespective of the accessibility of the parent node(s) + return tree.getPath(); + } else { + return property.getValue(STRING); + } + } + + /** + * The tree identified by the specified {@code identifier} or {@code null}. + * + * @param identifier The identifier of the Node such as exposed by {@link javax.jcr.Node#getIdentifier()} + * @return The tree with the given {@code identifier} or {@code null} if no + * such tree exists or isn't accessible to the content session. + */ + @CheckForNull + public Tree getTree(String identifier) { + if (isValidUUID(identifier)) { + String path = resolveUUID(identifier); + return (path == null) ? null : root.getTree(path); + } else { + // TODO as stated in NodeDelegate#getIdentifier() a non-uuid ID should + // TODO consisting of closest referenceable parent and a relative path + // TODO irrespective of the accessibility of the parent node(s) + return root.getTree(identifier); + } + } + + /** + * The path of the tree identified by the specified {@code identifier} or {@code null}. + * + * @param identifier The identifier of the Tree such as exposed by {@link javax.jcr.Node#getIdentifier()} + * @return The tree with the given {@code identifier} or {@code null} if no + * such tree exists or isn't accessible to the content session. + */ + @CheckForNull + public String getPath(String identifier) { + if (isValidUUID(identifier)) { + return resolveUUID(identifier); + } else { + // TODO as stated in NodeDelegate#getIdentifier() a non-uuid ID should + // TODO consisting of closest referenceable parent and a relative path + // TODO irrespective of the accessibility of the parent node(s) + Tree tree = root.getTree(identifier); + return tree == null ? null : tree.getPath(); + } + } + + /** + * Returns the path of the tree references by the specified (weak) + * reference {@code PropertyState}. + * + * @param referenceValue A (weak) reference value. + * @return The tree with the given {@code identifier} or {@code null} if no + * such tree exists or isn't accessible to the content session. + */ + @CheckForNull + public String getPath(PropertyState referenceValue) { + int type = referenceValue.getType().tag(); + if (type == PropertyType.REFERENCE || type == PropertyType.WEAKREFERENCE) { + return resolveUUID(referenceValue); + } else { + throw new IllegalArgumentException("Invalid value type"); + } + } + + /** + * Searches all reference properties to the specified {@code tree} that match + * the given name and node type constraints. + * + * @param weak if {@code true} only weak references are returned. Otherwise only + * hard references are returned. + * @param tree The tree for which references should be searched. + * @param propertyName A name constraint for the reference properties; + * {@code null} if no constraint should be enforced. + * @param nodeTypeNames Node type constraints to be enforced when using + * for reference properties. + * @return A set of oak paths of those reference properties referring to the + * specified {@code tree} and matching the constraints. + */ + @Nonnull + public Set getReferences(boolean weak, Tree tree, final String propertyName, final String... nodeTypeNames) { + if (!isReferenceable(tree)) { + return Collections.emptySet(); + } else { + try { + final String uuid = getIdentifier(tree); + String reference = weak ? PropertyType.TYPENAME_WEAKREFERENCE : PropertyType.TYPENAME_REFERENCE; + String pName = propertyName == null ? "*" : propertyName; // TODO: sanitize against injection attacks!? + Map bindings = Collections.singletonMap("uuid", PropertyValues.newString(uuid)); + + Result result = root.getQueryEngine().executeQuery( + "SELECT * FROM [nt:base] WHERE PROPERTY([" + pName + "], '" + reference + "') = $uuid", + Query.JCR_SQL2, Long.MAX_VALUE, 0, bindings, new NamePathMapper.Default()); + + Iterable paths = Iterables.transform(result.getRows(), + new Function() { + @Override + public String apply(ResultRow row) { + String pName = propertyName == null + ? findProperty(row.getPath(), uuid) + : propertyName; + return PathUtils.concat(row.getPath(), pName); + } + }); + + if (nodeTypeNames.length > 0) { + paths = Iterables.filter(paths, new Predicate() { + @Override + public boolean apply(String path) { + Tree tree = root.getTree(PathUtils.getParentPath(path)); + if (tree != null) { + for (String ntName : nodeTypeNames) { + if (hasType(tree, ntName)) { + return true; + } + } + } + return false; + } + }); + } + + return Sets.newHashSet(paths); + } catch (ParseException e) { + log.error("query failed", e); + return Collections.emptySet(); + } + } + } + + private String findProperty(String path, final String uuid) { + // TODO (OAK-220) PropertyState can only be accessed from parent tree + Tree tree = root.getTree(path); + assert tree != null; + final PropertyState refProp = Iterables.find(tree.getProperties(), new Predicate() { + @Override + public boolean apply(PropertyState pState) { + if (pState.isArray()) { + for (String value : pState.getValue(Type.STRINGS)) { + if (uuid.equals(value)) { + return true; + } + } + return false; + } + else { + return uuid.equals(pState.getValue(STRING)); + } + } + }); + + return refProp.getName(); + } + + private static boolean hasType(Tree tree, String ntName) { + // TODO use NodeType.isNodeType to determine type membership instead of equality on type names + PropertyState pType = tree.getProperty(JcrConstants.JCR_PRIMARYTYPE); + if (pType != null) { + String primaryType = pType.getValue(STRING); + if (ntName.equals(primaryType)) { + return true; + } + } + + PropertyState pMixin = tree.getProperty(JcrConstants.JCR_MIXINTYPES); + if (pMixin != null) { + for (String mixinType : pMixin.getValue(STRINGS)) { + if (ntName.equals(mixinType)) { + return true; + } + } + } + + return false; + } + + public boolean isReferenceable(Tree tree) { + // TODO add proper implementation include node type eval + return tree.hasProperty(JcrConstants.JCR_UUID); + } + + @CheckForNull + private String resolveUUID(String uuid) { + return resolveUUID(StringPropertyState.stringProperty("", uuid)); + } + + private String resolveUUID(PropertyState uuid) { + try { + Map bindings = Collections.singletonMap("id", PropertyValues.create(uuid)); + Result result = root.getQueryEngine().executeQuery( + "SELECT * FROM [nt:base] WHERE [jcr:uuid] = $id", Query.JCR_SQL2, + Long.MAX_VALUE, 0, bindings, new NamePathMapper.Default()); + + String path = null; + for (ResultRow rr : result.getRows()) { + if (path != null) { + log.error("multiple results for identifier lookup: " + path + " vs. " + rr.getPath()); + return null; + } else { + path = rr.getPath(); + } + } + return path; + } catch (ParseException ex) { + log.error("query failed", ex); + return null; + } + } + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/CompositeIndexHookProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/CompositeIndexHookProvider.java new file mode 100644 index 00000000000..42d2b109d03 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/CompositeIndexHookProvider.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +public class CompositeIndexHookProvider implements IndexHookProvider { + + @Nonnull + public static IndexHookProvider compose( + @Nonnull Collection providers) { + if (providers.isEmpty()) { + return new IndexHookProvider() { + @Override + public List getIndexHooks(String type, + NodeBuilder builder) { + return ImmutableList.of(); + } + }; + } else if (providers.size() == 1) { + return providers.iterator().next(); + } else { + return new CompositeIndexHookProvider( + ImmutableList.copyOf(providers)); + } + } + + private final List providers; + + private CompositeIndexHookProvider(List providers) { + this.providers = providers; + } + + public CompositeIndexHookProvider(IndexHookProvider... providers) { + this(Arrays.asList(providers)); + } + + @Override + @Nonnull + public List getIndexHooks(String type, + NodeBuilder builder) { + List indexes = Lists.newArrayList(); + for (IndexHookProvider provider : providers) { + indexes.addAll(provider.getIndexHooks(type, builder)); + } + return indexes; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/CompositeNodeStateDiff.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/CompositeNodeStateDiff.java new file mode 100644 index 00000000000..2bf8e174ad1 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/CompositeNodeStateDiff.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.plugins.index; + +import java.util.Arrays; +import java.util.List; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; + +public class CompositeNodeStateDiff implements NodeStateDiff { + + private final List providers; + + public CompositeNodeStateDiff(List providers) { + this.providers = providers; + } + + public CompositeNodeStateDiff(NodeStateDiff... providers) { + this(Arrays.asList(providers)); + } + + @Override + public void propertyAdded(PropertyState after) { + for (NodeStateDiff d : providers) { + d.propertyAdded(after); + } + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) { + for (NodeStateDiff d : providers) { + d.propertyChanged(before, after); + } + } + + @Override + public void propertyDeleted(PropertyState before) { + for (NodeStateDiff d : providers) { + d.propertyDeleted(before); + } + } + + @Override + public void childNodeAdded(String name, NodeState after) { + for (NodeStateDiff d : providers) { + d.childNodeAdded(name, after); + } + } + + @Override + public void childNodeChanged(String name, NodeState before, NodeState after) { + for (NodeStateDiff d : providers) { + d.childNodeChanged(name, before, after); + } + } + + @Override + public void childNodeDeleted(String name, NodeState before) { + for (NodeStateDiff d : providers) { + d.childNodeDeleted(name, before); + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexConstants.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexConstants.java new file mode 100644 index 00000000000..f362faa4892 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexConstants.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index; + +public interface IndexConstants { + + String INDEX_DEFINITIONS_NODE_TYPE = "oak:queryIndexDefinition"; + + String INDEX_DEFINITIONS_NAME = "oak:index"; + + String TYPE_PROPERTY_NAME = "type"; + + String TYPE_UNKNOWN = "unknown"; + + String REINDEX_PROPERTY_NAME = "reindex"; + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexDefinition.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexDefinition.java new file mode 100644 index 00000000000..c023bffb1ee --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexDefinition.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index; + +import javax.annotation.Nonnull; + +/** + * Defines an index definition + * + */ +public interface IndexDefinition { + + /** + * Get the unique index name. This is also the name of the index node. + * + * @return the index name + */ + @Nonnull + String getName(); + + /** + * @return the index path, including the name as the last segment + */ + @Nonnull + String getPath(); + + /** + * @return the index type + */ + @Nonnull + String getType(); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexDefinitionImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexDefinitionImpl.java new file mode 100644 index 00000000000..43e88fd4443 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexDefinitionImpl.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index; + +public class IndexDefinitionImpl implements IndexDefinition, IndexConstants { + + private final String name; + private final String type; + private final String path; + + public IndexDefinitionImpl(String name, String type, String path) { + this.name = name; + this.type = type; + this.path = path; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getType() { + return type; + } + + @Override + public String getPath() { + return path; + } + + @Override + public String toString() { + return "IndexDefinitionImpl [name=" + name + ", type=" + type + + ", path=" + path + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((path == null) ? 0 : path.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + IndexDefinitionImpl other = (IndexDefinitionImpl) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (path == null) { + if (other.path != null) + return false; + } else if (!path.equals(other.path)) + return false; + if (type == null) { + if (other.type != null) + return false; + } else if (!type.equals(other.type)) + return false; + return true; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexHook.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexHook.java new file mode 100644 index 00000000000..399ffbd1d00 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexHook.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.plugins.index; + +import java.io.Closeable; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; + +/** + * Represents the content of a QueryIndex as well as a mechanism for keeping + * this content up to date. + * + */ +public interface IndexHook extends Closeable { + + NodeStateDiff preProcess() throws CommitFailedException; + + void postProcess() throws CommitFailedException; + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexHookManager.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexHookManager.java new file mode 100644 index 00000000000..64ae5f561d6 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexHookManager.java @@ -0,0 +1,249 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index; + +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; +import static org.apache.jackrabbit.oak.commons.PathUtils.concat; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NODE_TYPE; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.TYPE_PROPERTY_NAME; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.TYPE_UNKNOWN; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.REINDEX_PROPERTY_NAME; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeState; +import org.apache.jackrabbit.oak.spi.commit.CommitHook; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; +import org.apache.jackrabbit.oak.spi.state.NodeStateUtils; + +/** + * Keeps existing IndexHooks updated. + * + *

+ * The existing index list is obtained via the IndexHookProvider. + *

+ * + * @see IndexHook + * @see IndexHookProvider + * + */ +public class IndexHookManager implements CommitHook { + + private final IndexHookProvider provider; + + public IndexHookManager(IndexHookProvider provider) { + this.provider = provider; + } + + @Override + public NodeState processCommit(NodeState before, NodeState after) + throws CommitFailedException { + NodeBuilder builder = after.builder(); + + // , + Map allDefs = new HashMap(); + after.compareAgainstBaseState(before, + new IndexDefDiff(builder, allDefs)); + + // , > + Map> updates = new HashMap>(); + for (String def : allDefs.keySet()) { + NodeBuilder cb = allDefs.get(def); + String type = TYPE_UNKNOWN; + PropertyState typePS = cb.getProperty(TYPE_PROPERTY_NAME); + if (typePS != null && !typePS.isArray()) { + type = typePS.getValue(Type.STRING); + } + Map defs = updates.get(type); + if (defs == null) { + defs = new HashMap(); + updates.put(type, defs); + } + defs.put(def, cb); + } + + // commit + List hooks = new ArrayList(); + List reindexHooks = new ArrayList(); + + for (String type : updates.keySet()) { + Map update = updates.get(type); + for (String path : update.keySet()) { + NodeBuilder updateBuiler = update.get(path); + boolean reindex = getAndResetReindex(updateBuiler); + if (reindex) { + reindexHooks.addAll(provider.getIndexHooks(type, + updateBuiler)); + } else { + hooks.addAll(provider.getIndexHooks(type, updateBuiler)); + } + } + } + processIndexHooks(reindexHooks, MemoryNodeState.EMPTY_NODE, after); + processIndexHooks(hooks, before, after); + return builder.getNodeState(); + } + + private void processIndexHooks(List hooks, NodeState before, + NodeState after) throws CommitFailedException { + try { + + List diffs = new ArrayList(); + for (IndexHook hook : hooks) { + diffs.add(hook.preProcess()); + } + after.compareAgainstBaseState(before, new CompositeNodeStateDiff( + diffs)); + for (IndexHook hook : hooks) { + hook.postProcess(); + } + + } finally { + for (IndexHook hook : hooks) { + try { + hook.close(); + } catch (IOException e) { + e.printStackTrace(); + throw new CommitFailedException( + "Failed to close the index hook", e); + } + } + } + } + + protected static boolean getAndResetReindex(NodeBuilder builder) { + boolean isReindex = false; + PropertyState ps = builder.getProperty(REINDEX_PROPERTY_NAME); + if (ps != null) { + isReindex = ps.getValue(Type.BOOLEAN); + builder.setProperty(REINDEX_PROPERTY_NAME, false); + } + return isReindex; + } + + protected static class IndexDefDiff implements NodeStateDiff { + + private final IndexDefDiff parent; + + private final NodeBuilder node; + + private final String name; + + private String path; + + private final Map updates; + + public IndexDefDiff(IndexDefDiff parent, NodeBuilder node, String name, + String path, Map updates) { + this.parent = parent; + this.node = node; + this.name = name; + this.path = path; + this.updates = updates; + + if (node != null + && isIndexNodeType(node.getProperty(JCR_PRIMARYTYPE))) { + node.setProperty(REINDEX_PROPERTY_NAME, true); + this.updates.put(getPath(), node); + } + + if (node != null && node.hasChildNode(INDEX_DEFINITIONS_NAME)) { + NodeBuilder index = node.child(INDEX_DEFINITIONS_NAME); + for (String indexName : index.getChildNodeNames()) { + String indexPath = concat(getPath(), + INDEX_DEFINITIONS_NAME, indexName); + NodeBuilder indexChild = index.child(indexName); + if (isIndexNodeType(indexChild.getProperty(JCR_PRIMARYTYPE))) { + this.updates.put(indexPath, indexChild); + } + } + } + } + + public IndexDefDiff(NodeBuilder root, Map updates) { + this(null, root, null, "/", updates); + } + + public IndexDefDiff(IndexDefDiff parent, String name) { + this(parent, getChildNode(parent.node, name), name, null, + parent.updates); + } + + private static NodeBuilder getChildNode(NodeBuilder node, String name) { + if (node != null && node.hasChildNode(name)) { + return node.child(name); + } else { + return null; + } + } + + private String getPath() { + if (path == null) { // => parent != null + path = concat(parent.getPath(), name); + } + return path; + } + + @Override + public void propertyAdded(PropertyState after) { + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) { + } + + @Override + public void propertyDeleted(PropertyState before) { + } + + @Override + public void childNodeAdded(String name, NodeState after) { + childNodeChanged(name, MemoryNodeState.EMPTY_NODE, after); + } + + @Override + public void childNodeChanged(String name, NodeState before, + NodeState after) { + if (NodeStateUtils.isHidden(name)) { + return; + } + after.compareAgainstBaseState(before, new IndexDefDiff(this, name)); + } + + @Override + public void childNodeDeleted(String name, NodeState before) { + } + + private boolean isIndexNodeType(PropertyState ps) { + return ps != null + && !ps.isArray() + && ps.getValue(Type.STRING).equals( + INDEX_DEFINITIONS_NODE_TYPE); + } + + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexHookProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexHookProvider.java new file mode 100644 index 00000000000..8d120690f8e --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexHookProvider.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index; + +import java.util.List; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; + +/** + * Extension point for plugging in different kinds of IndexHook providers. + * + * @see IndexHook + */ +public interface IndexHookProvider { + + /** + * + * Each provider knows how to produce a certain type of index. If the + * type param is of an unknown value, the provider is expected + * to return an empty list. + * + * @param type + * the index type + * @param builder + * the node state builder that will be used for updates + * @return a list of index hooks + */ + @Nonnull + List getIndexHooks(String type, NodeBuilder builder); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexUtils.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexUtils.java new file mode 100644 index 00000000000..79143a6a0e9 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexUtils.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index; + +import static org.apache.jackrabbit.oak.api.Type.STRING; +import static org.apache.jackrabbit.oak.commons.PathUtils.concat; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +public class IndexUtils implements IndexConstants { + + /** + * Builds a list of the existing index definitions. + * + * Checks only children of the provided state for an index definitions + * container node, aka a node named {@link INDEX_DEFINITIONS_NAME} + * + * @return + */ + public static List buildIndexDefinitions(NodeState state, + String indexConfigPath, String typeFilter) { + NodeState definitions = state.getChildNode(INDEX_DEFINITIONS_NAME); + if (definitions == null) { + return Collections.emptyList(); + } + indexConfigPath = concat(indexConfigPath, INDEX_DEFINITIONS_NAME); + + List defs = new ArrayList(); + for (ChildNodeEntry c : definitions.getChildNodeEntries()) { + IndexDefinition def = getDefinition(indexConfigPath, c, typeFilter); + if (def == null) { + continue; + } + defs.add(def); + } + return defs; + } + + /** + * Builds an {@link IndexDefinition} out of a {@link ChildNodeEntry} + * + */ + private static IndexDefinition getDefinition(String path, + ChildNodeEntry def, String typeFilter) { + String name = def.getName(); + NodeState ns = def.getNodeState(); + PropertyState typeProp = ns.getProperty(TYPE_PROPERTY_NAME); + String type = TYPE_UNKNOWN; + if (typeProp != null && !typeProp.isArray()) { + type = typeProp.getValue(STRING); + } + if (typeFilter != null && !typeFilter.equals(type)) { + return null; + } + return new IndexDefinitionImpl(name, type, concat(path, name)); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/FieldFactory.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/FieldFactory.java new file mode 100644 index 00000000000..5e3ef4d5332 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/FieldFactory.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.lucene; + +import org.apache.lucene.document.Field; +import org.apache.lucene.document.StringField; + +import static org.apache.jackrabbit.oak.plugins.index.lucene.FieldNames.PATH; +import static org.apache.lucene.document.Field.Store.NO; +import static org.apache.lucene.document.Field.Store.YES; + +/** + * {@code FieldFactory} is a factory for Field instances with + * frequently used fields. + */ +public final class FieldFactory { + + /** + * Private constructor. + */ + private FieldFactory() { + } + + public static Field newPathField(String path) { + return new StringField(PATH, path, YES); + } + + public static Field newPropertyField(String name, String value) { + // TODO do we need norms info on the indexed fields ? TextField:StringField + // return new TextField(name, value, NO); + return new StringField(name, value, NO); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/FieldNames.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/FieldNames.java new file mode 100644 index 00000000000..74ae81205f6 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/FieldNames.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.plugins.index.lucene; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * Defines field names that are used internally to store :path, etc in the + * search index. + */ +public final class FieldNames { + + /** + * Private constructor. + */ + private FieldNames() { + } + + /** + * Name of the field that contains the {@value} property of the node. + */ + public static final String PATH = ":path"; + + /** + * Used to select only the PATH field from the lucene documents + */ + public static final Set PATH_SELECTOR = new HashSet( + Arrays.asList(PATH)); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneEditor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneEditor.java new file mode 100644 index 00000000000..b7588fec515 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneEditor.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.lucene; + +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; + +import java.io.IOException; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.index.IndexHook; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.util.Version; + +/** + * This class updates a Lucene index when node content is changed. + */ +class LuceneEditor implements IndexHook, LuceneIndexConstants { + + private static final Version VERSION = Version.LUCENE_40; + + private static final Analyzer ANALYZER = new StandardAnalyzer(VERSION); + + private static final IndexWriterConfig config = getIndexWriterConfig(); + + private LuceneIndexDiff diff; + + private static IndexWriterConfig getIndexWriterConfig() { + // FIXME: Hack needed to make Lucene work in an OSGi environment + Thread thread = Thread.currentThread(); + ClassLoader loader = thread.getContextClassLoader(); + thread.setContextClassLoader(IndexWriterConfig.class.getClassLoader()); + try { + return new IndexWriterConfig(VERSION, ANALYZER); + } finally { + thread.setContextClassLoader(loader); + } + } + + private final NodeBuilder root; + + public LuceneEditor(NodeBuilder root) { + this.root = root; + } + + // -----------------------------------------------------< IndexHook >-- + + @Override + public NodeStateDiff preProcess() throws CommitFailedException { + try { + IndexWriter writer = new IndexWriter(new ReadWriteOakDirectory( + getIndexNode(root).child(INDEX_DATA_CHILD_NAME)), config); + diff = new LuceneIndexDiff(writer, root, "/"); + return diff; + } catch (IOException e) { + e.printStackTrace(); + throw new CommitFailedException( + "Failed to create writer for the full text search index", e); + } + } + + private static NodeBuilder getIndexNode(NodeBuilder node) { + if (node != null && node.hasChildNode(INDEX_DEFINITIONS_NAME)) { + NodeBuilder index = node.child(INDEX_DEFINITIONS_NAME); + for (String indexName : index.getChildNodeNames()) { + NodeBuilder child = index.child(indexName); + if (isIndexNodeType(child.getProperty(JCR_PRIMARYTYPE)) + && isIndexType(child.getProperty(TYPE_PROPERTY_NAME), + TYPE_LUCENE)) { + return child; + } + } + } + // did not find a proper child, will use root directly + return node; + } + + private static boolean isIndexNodeType(PropertyState ps) { + return ps != null && !ps.isArray() + && ps.getValue(Type.STRING).equals(INDEX_DEFINITIONS_NODE_TYPE); + } + + private static boolean isIndexType(PropertyState ps, String type) { + return ps != null && !ps.isArray() + && ps.getValue(Type.STRING).equals(type); + } + + @Override + public void postProcess() throws CommitFailedException { + try { + diff.postProcess(); + } catch (IOException e) { + e.printStackTrace(); + throw new CommitFailedException( + "Failed to update the full text search index", e); + } + } + + @Override + public void close() throws IOException { + diff.close(); + diff = null; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneHook.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneHook.java new file mode 100644 index 00000000000..d21eeb40bdc --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneHook.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.plugins.index.lucene; + +import java.io.IOException; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.plugins.index.IndexHook; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; + +/** + * {@link IndexHook} implementation that is responsible for keeping the + * {@link LuceneIndex} up to date + * + * @see LuceneIndex + * + */ +public class LuceneHook implements IndexHook { + + private final NodeBuilder builder; + + private IndexHook luceneEditor; + + public LuceneHook(NodeBuilder builder) { + this.builder = builder; + } + + // -----------------------------------------------------< IndexHook >-- + + @Override + @Nonnull + public NodeStateDiff preProcess() throws CommitFailedException { + luceneEditor = new LuceneEditor(builder); + return luceneEditor.preProcess(); + } + + @Override + public void postProcess() throws CommitFailedException { + luceneEditor.postProcess(); + } + + @Override + public void close() throws IOException { + try { + if (luceneEditor != null) { + luceneEditor.close(); + } + } finally { + luceneEditor = null; + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndex.java new file mode 100644 index 00000000000..f8f91575d10 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndex.java @@ -0,0 +1,398 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.lucene; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import javax.annotation.CheckForNull; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; + +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.core.ReadOnlyTree; +import org.apache.jackrabbit.oak.plugins.index.IndexDefinition; +import org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants; +import org.apache.jackrabbit.oak.plugins.nodetype.ReadOnlyNodeTypeManager; +import org.apache.jackrabbit.oak.query.index.IndexRowImpl; +import org.apache.jackrabbit.oak.spi.query.Cursor; +import org.apache.jackrabbit.oak.spi.query.Filter; +import org.apache.jackrabbit.oak.spi.query.Filter.PropertyRestriction; +import org.apache.jackrabbit.oak.spi.query.IndexRow; +import org.apache.jackrabbit.oak.spi.query.QueryIndex; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.ReadOnlyBuilder; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.MultiFields; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.BooleanClause.Occur; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.PrefixQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TermRangeQuery; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.search.WildcardQuery; +import org.apache.lucene.store.Directory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.jackrabbit.oak.commons.PathUtils.elements; +import static org.apache.jackrabbit.oak.plugins.index.lucene.FieldNames.PATH; +import static org.apache.jackrabbit.oak.plugins.index.lucene.FieldNames.PATH_SELECTOR; +import static org.apache.jackrabbit.oak.plugins.index.lucene.TermFactory.newPathTerm; +import static org.apache.jackrabbit.oak.query.Query.JCR_PATH; + +/** + * Provides a QueryIndex that does lookups against a Lucene-based index + * + *

+ * To define a lucene index on a subtree you have to add an oak:index node. + * + * Under it follows the index definition node that: + *

    + *
  • must be of type oak:queryIndexDefinition
  • + *
  • must have the type property set to lucene
  • + *
+ *

+ * + *

+ * Note: reindex is a property that when set to true, triggers a full content reindex. + *

+ * + *
+ * 
+ * {
+ *     NodeBuilder index = root.child("oak:index");
+ *     index.child("lucene")
+ *         .setProperty("jcr:primaryType", "oak:queryIndexDefinition", Type.NAME)
+ *         .setProperty("type", "lucene")
+ *         .setProperty("reindex", "true");
+ * }
+ * 
+ * 
+ * + * @see QueryIndex + * + */ +public class LuceneIndex implements QueryIndex, LuceneIndexConstants { + + private static final Logger LOG = LoggerFactory + .getLogger(LuceneIndex.class); + + private final IndexDefinition index; + + public LuceneIndex(IndexDefinition indexDefinition) { + this.index = indexDefinition; + } + + @Override + public String getIndexName() { + return index.getName(); + } + + @Override + public double getCost(Filter filter, NodeState root) { + // TODO: proper cost calculation + return 1.0; + } + + @Override + public String getPlan(Filter filter, NodeState root) { + return getQuery(filter, root, null).toString(); + } + + @Override + public Cursor query(Filter filter, NodeState root) { + + NodeBuilder builder = new ReadOnlyBuilder(root); + for (String name : elements(index.getPath())) { + builder = builder.child(name); + } + if (!builder.hasChildNode(INDEX_DATA_CHILD_NAME)) { + // index not initialized yet + return new PathCursor(Collections. emptySet()); + } + builder = builder.child(INDEX_DATA_CHILD_NAME); + + Directory directory = new ReadOnlyOakDirectory(builder); + long s = System.currentTimeMillis(); + + try { + try { + IndexReader reader = DirectoryReader.open(directory); + try { + IndexSearcher searcher = new IndexSearcher(reader); + Collection paths = new ArrayList(); + + Query query = getQuery(filter, root, reader); + if (query != null) { + TopDocs docs = searcher + .search(query, Integer.MAX_VALUE); + for (ScoreDoc doc : docs.scoreDocs) { + String path = reader.document(doc.doc, + PATH_SELECTOR).get(PATH); + if ("".equals(path)) { + paths.add("/"); + } else if (path != null) { + paths.add(path); + } + } + } + LOG.debug("query via {} took {} ms.", this, + System.currentTimeMillis() - s); + return new PathCursor(paths); + } finally { + reader.close(); + } + } finally { + directory.close(); + } + } catch (IOException e) { + e.printStackTrace(); + return new PathCursor(Collections. emptySet()); + } + } + + private static Query getQuery(Filter filter, NodeState root, IndexReader reader) { + List qs = new ArrayList(); + + try { + addNodeTypeConstraints(qs, filter.getNodeType(), root); + } catch (RepositoryException e) { + throw new RuntimeException( + "Unable to process node type constraints", e); + } + + String path = filter.getPath(); + switch (filter.getPathRestriction()) { + case ALL_CHILDREN: + if ("/".equals(path)) { + break; + } + if (!path.endsWith("/")) { + path += "/"; + } + qs.add(new PrefixQuery(newPathTerm(path))); + break; + case DIRECT_CHILDREN: + // FIXME + if (!path.endsWith("/")) { + path += "/"; + } + qs.add(new PrefixQuery(newPathTerm(path))); + break; + case EXACT: + qs.add(new TermQuery(newPathTerm(path))); + break; + case PARENT: + if (PathUtils.denotesRoot(path)) { + // there's no parent of the root node + return null; + } + qs.add(new TermQuery(newPathTerm(PathUtils.getParentPath(path)))); + break; + } + + for (PropertyRestriction pr : filter.getPropertyRestrictions()) { + String name = pr.propertyName; + if (name.contains("/")) { + // lucene cannot handle child-level property restrictions + continue; + } + + String first = null; + String last = null; + boolean isLike = pr.isLike; + + // TODO what to do with escaped tokens? + if (pr.first != null) { + first = pr.first.getValue(Type.STRING); + first = first.replace("\\", ""); + } + if (pr.last != null) { + last = pr.last.getValue(Type.STRING); + last = last.replace("\\", ""); + } + + if (isLike) { + first = first.replace('%', WildcardQuery.WILDCARD_STRING); + first = first.replace('_', WildcardQuery.WILDCARD_CHAR); + + int indexOfWS = first.indexOf(WildcardQuery.WILDCARD_STRING); + int indexOfWC = first.indexOf(WildcardQuery.WILDCARD_CHAR); + int len = first.length(); + + if (indexOfWS == len || indexOfWC == len) { + // remove trailing "*" for prefixquery + first = first.substring(0, first.length() - 1); + if (JCR_PATH.equals(name)) { + qs.add(new PrefixQuery(newPathTerm(first))); + } else { + qs.add(new PrefixQuery(new Term(name, first))); + } + } else { + if (JCR_PATH.equals(name)) { + qs.add(new WildcardQuery(newPathTerm(first))); + } else { + qs.add(new WildcardQuery(new Term(name, first))); + } + } + continue; + } + + if (first != null && first.equals(last) && pr.firstIncluding + && pr.lastIncluding) { + if (JCR_PATH.equals(name)) { + qs.add(new TermQuery(newPathTerm(first))); + } else { + if ("*".equals(name)) { + addReferenceConstraint(first, qs, reader); + } else { + qs.add(new TermQuery(new Term(name, first))); + } + } + continue; + } + + qs.add(TermRangeQuery.newStringRange(name, first, last, + pr.firstIncluding, pr.lastIncluding)); + + } + + if (qs.size() == 0) { + return new MatchAllDocsQuery(); + } + if (qs.size() == 1) { + return qs.get(0); + } + BooleanQuery bq = new BooleanQuery(); + for (Query q : qs) { + bq.add(q, Occur.MUST); + } + return bq; + } + + private static void addReferenceConstraint(String uuid, List qs, + IndexReader reader) { + if (reader == null) { + // getPlan call + qs.add(new TermQuery(new Term("*", uuid))); + return; + } + + // reference query + BooleanQuery bq = new BooleanQuery(); + Collection fields = MultiFields.getIndexedFields(reader); + for (String f : fields) { + bq.add(new TermQuery(new Term(f, uuid)), Occur.SHOULD); + } + qs.add(bq); + } + + private static void addNodeTypeConstraints( + List qs, String name, NodeState root) + throws RepositoryException { + // TODO remove empty name check once OAK-359 is done + if (NodeTypeConstants.NT_BASE.equals(name) || "".equals(name)) { + return; // shortcut + } + NodeState system = root.getChildNode(NodeTypeConstants.JCR_SYSTEM); + if (system == null) { + return; + } + final NodeState types = + system.getChildNode(NodeTypeConstants.JCR_NODE_TYPES); + if (types == null) { + return; + } + + NodeTypeManager manager = new ReadOnlyNodeTypeManager() { + @Override @CheckForNull + protected Tree getTypes() { + return new ReadOnlyTree(types); + } + }; + + BooleanQuery bq = new BooleanQuery(); + NodeType type = manager.getNodeType(name); + bq.add(createNodeTypeQuery(type), Occur.SHOULD); + NodeTypeIterator iterator = type.getSubtypes(); + while (iterator.hasNext()) { + bq.add(createNodeTypeQuery(iterator.nextNodeType()), Occur.SHOULD); + } + qs.add(bq); + } + + private static Query createNodeTypeQuery(NodeType type) { + String name = NodeTypeConstants.JCR_PRIMARYTYPE; + if (type.isMixin()) { + name = NodeTypeConstants.JCR_MIXINTYPES; + } + return new TermQuery(new Term(name, type.getName())); + } + + /** + * A cursor over the resulting paths. + */ + private static class PathCursor implements Cursor { + + private final Iterator iterator; + + private String path; + + public PathCursor(Collection paths) { + this.iterator = paths.iterator(); + } + + @Override + public boolean next() { + if (iterator.hasNext()) { + path = iterator.next(); + return true; + } else { + path = null; + return false; + } + } + + @Override + public IndexRow currentRow() { + // TODO support jcr:score and possibly rep:exceprt + return new IndexRowImpl(path); + } + + } + + @Override + public String toString() { + return "LuceneIndex [index=" + index + "]"; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexConstants.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexConstants.java new file mode 100644 index 00000000000..ab585b49592 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexConstants.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.lucene; + +import org.apache.jackrabbit.oak.plugins.index.IndexConstants; + +public interface LuceneIndexConstants extends IndexConstants { + + String TYPE_LUCENE = "lucene"; + + String INDEX_DATA_CHILD_NAME = ":data"; + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexDiff.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexDiff.java new file mode 100644 index 00000000000..a843ab79ad1 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexDiff.java @@ -0,0 +1,191 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.lucene; + +import static org.apache.jackrabbit.oak.commons.PathUtils.concat; +import static org.apache.jackrabbit.oak.plugins.index.lucene.FieldFactory.newPathField; +import static org.apache.jackrabbit.oak.plugins.index.lucene.FieldFactory.newPropertyField; +import static org.apache.jackrabbit.oak.plugins.index.lucene.TermFactory.newPathTerm; + +import java.io.Closeable; +import java.io.IOException; + +import javax.jcr.PropertyType; + +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; +import org.apache.jackrabbit.oak.spi.state.NodeStateUtils; +import org.apache.lucene.document.Document; +import org.apache.lucene.index.IndexWriter; +import org.apache.tika.Tika; +import org.apache.tika.exception.TikaException; + +public class LuceneIndexDiff implements NodeStateDiff, Closeable { + + private static final Tika TIKA = new Tika(); + + private final IndexWriter writer; + + private final NodeBuilder node; + + private final String path; + + private boolean modified; + + private IOException exception; + + public LuceneIndexDiff(IndexWriter writer, NodeBuilder node, String path) { + this.writer = writer; + this.node = node; + this.path = path; + } + + public void postProcess() throws IOException { + if (exception != null) { + throw exception; + } + if (modified) { + writer.updateDocument(newPathTerm(path), + makeDocument(path, node.getNodeState())); + } + } + + // -----------------------------------------------------< NodeStateDiff >-- + + @Override + public void propertyAdded(PropertyState after) { + modified = true; + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) { + modified = true; + } + + @Override + public void propertyDeleted(PropertyState before) { + modified = true; + } + + @Override + public void childNodeAdded(String name, NodeState after) { + if (NodeStateUtils.isHidden(name)) { + return; + } + if (exception == null) { + try { + addSubtree(concat(path, name), after); + } catch (IOException e) { + exception = e; + } + } + } + + private void addSubtree(String path, NodeState state) throws IOException { + writer.updateDocument(newPathTerm(path), makeDocument(path, state)); + for (ChildNodeEntry entry : state.getChildNodeEntries()) { + if (NodeStateUtils.isHidden(entry.getName())) { + continue; + } + addSubtree(concat(path, entry.getName()), entry.getNodeState()); + } + } + + @Override + public void childNodeChanged(String name, NodeState before, NodeState after) { + if (NodeStateUtils.isHidden(name)) { + return; + } + if (exception == null && node.hasChildNode(name)) { + LuceneIndexDiff diff = new LuceneIndexDiff(writer, + node.child(name), concat(path, name)); + after.compareAgainstBaseState(before, diff); + try { + diff.postProcess(); + } catch (IOException e) { + exception = e; + } + } + } + + @Override + public void childNodeDeleted(String name, NodeState before) { + if (NodeStateUtils.isHidden(name)) { + return; + } + if (exception == null) { + try { + deleteSubtree(concat(path, name), before); + } catch (IOException e) { + exception = e; + } + } + } + + private void deleteSubtree(String path, NodeState state) throws IOException { + writer.deleteDocuments(newPathTerm(path)); + for (ChildNodeEntry entry : state.getChildNodeEntries()) { + if (NodeStateUtils.isHidden(entry.getName())) { + continue; + } + deleteSubtree(concat(path, entry.getName()), entry.getNodeState()); + } + } + + private static Document makeDocument(String path, NodeState state) { + Document document = new Document(); + document.add(newPathField(path)); + for (PropertyState property : state.getProperties()) { + String pname = property.getName(); + switch (property.getType().tag()) { + case PropertyType.BINARY: + for (Blob v : property.getValue(Type.BINARIES)) { + document.add(newPropertyField(pname, parseStringValue(v))); + } + break; + default: + for (String v : property.getValue(Type.STRINGS)) { + document.add(newPropertyField(pname, v)); + } + break; + } + } + return document; + } + + private static String parseStringValue(Blob v) { + try { + return TIKA.parseToString(v.getNewStream()); + } catch (IOException e) { + } catch (TikaException e) { + } + return ""; + } + + // -----------------------------------------------------< Closeable >-- + + @Override + public void close() throws IOException { + writer.close(); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexHookProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexHookProvider.java new file mode 100644 index 00000000000..6eb60be7c20 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexHookProvider.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.plugins.index.lucene; + +import static org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstants.TYPE_LUCENE; + +import java.util.List; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.apache.jackrabbit.oak.plugins.index.IndexHook; +import org.apache.jackrabbit.oak.plugins.index.IndexHookProvider; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; + +import com.google.common.collect.ImmutableList; + +/** + * Service that provides Lucene based IndexHooks + * + * @see LuceneHook + * @see IndexHookProvider + * + */ +@Component +@Service(IndexHookProvider.class) +public class LuceneIndexHookProvider implements IndexHookProvider { + + @Override + public List getIndexHooks(String type, + NodeBuilder builder) { + if (TYPE_LUCENE.equals(type)) { + return ImmutableList.of(new LuceneHook(builder)); + } + return ImmutableList.of(); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexProvider.java new file mode 100644 index 00000000000..cfe9ef5dced --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexProvider.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.plugins.index.lucene; + +import static org.apache.jackrabbit.oak.plugins.index.IndexUtils.buildIndexDefinitions; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.plugins.index.IndexDefinition; +import org.apache.jackrabbit.oak.spi.query.QueryIndex; +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A provider for Lucene indexes. + * + * @see LuceneIndex + * + */ +public class LuceneIndexProvider implements QueryIndexProvider, + LuceneIndexConstants { + + private static final Logger LOG = LoggerFactory + .getLogger(LuceneIndexProvider.class); + + @Override @Nonnull + public List getQueryIndexes(NodeState nodeState) { + List tempIndexes = new ArrayList(); + for (IndexDefinition child : buildIndexDefinitions(nodeState, "/", + TYPE_LUCENE)) { + LOG.debug("found a lucene index definition {}", child); + tempIndexes.add(new LuceneIndex(child)); + } + return tempIndexes; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/ReadOnlyOakDirectory.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/ReadOnlyOakDirectory.java new file mode 100644 index 00000000000..b666090ab9c --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/ReadOnlyOakDirectory.java @@ -0,0 +1,192 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.lucene; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; + +import javax.jcr.UnsupportedRepositoryOperationException; + +import com.google.common.collect.Iterables; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.IOContext; +import org.apache.lucene.store.IndexInput; +import org.apache.lucene.store.IndexOutput; +import org.apache.lucene.store.NoLockFactory; + +import static org.apache.jackrabbit.oak.api.Type.BINARY; + +/** + * A read-only implementation of the Lucene {@link Directory} (a flat list of + * files) that only allows reading of the Lucene index content stored in an Oak + * repository. + */ +class ReadOnlyOakDirectory extends Directory { + + protected final NodeBuilder directoryBuilder; + + public ReadOnlyOakDirectory(NodeBuilder directoryBuilder) { + this.lockFactory = NoLockFactory.getNoLockFactory(); + this.directoryBuilder = directoryBuilder; + } + + @Override + public String[] listAll() throws IOException { + return Iterables.toArray( + directoryBuilder.getChildNodeNames(), String.class); + } + + @Override + public boolean fileExists(String name) throws IOException { + return directoryBuilder.hasChildNode(name); + } + + @Override + public void deleteFile(String name) throws IOException { + directoryBuilder.removeNode(name); + } + + @Override + public long fileLength(String name) throws IOException { + if (!fileExists(name)) { + return 0; + } + + NodeBuilder fileBuilder = directoryBuilder.child(name); + PropertyState property = fileBuilder.getProperty("jcr:data"); + if (property == null || property.isArray()) { + return 0; + } + + return property.size(); + } + + @Override + public IndexOutput createOutput(String name, IOContext context) + throws IOException { + throw new IOException(new UnsupportedRepositoryOperationException()); + } + + @Override + public IndexInput openInput(String name, IOContext context) + throws IOException { + return new OakIndexInput(name); + } + + @Override + public void sync(Collection names) throws IOException { + // ? + } + + @Override + public void close() throws IOException { + // do nothing + } + + protected byte[] readFile(String name) throws IOException { + if (!fileExists(name)) { + return new byte[0]; + } + + NodeBuilder fileBuilder = directoryBuilder.child(name); + PropertyState property = fileBuilder.getProperty("jcr:data"); + if (property == null || property.isArray()) { + return new byte[0]; + } + + InputStream stream = property.getValue(BINARY).getNewStream(); + try { + byte[] buffer = new byte[(int) property.size()]; + + int size = 0; + do { + int n = stream.read(buffer, size, buffer.length - size); + if (n == -1) { + throw new IOException( + "Unexpected end of index file: " + name); + } + size += n; + } while (size < buffer.length); + + return buffer; + } finally { + stream.close(); + } + } + + private final class OakIndexInput extends IndexInput { + + private final byte[] data; + + private int position; + + public OakIndexInput(String name) throws IOException { + super(name); + this.data = readFile(name); + this.position = 0; + } + + @Override + public void readBytes(byte[] b, int offset, int len) + throws IOException { + if (len < 0 || position + len > data.length) { + throw new IOException("Invalid byte range request"); + } else { + System.arraycopy(data, position, b, offset, len); + position += len; + } + } + + @Override + public byte readByte() throws IOException { + if (position >= data.length) { + throw new IOException("Invalid byte range request"); + } else { + return data[position++]; + } + } + + @Override + public void seek(long pos) throws IOException { + if (pos < 0 || pos >= data.length) { + throw new IOException("Invalid seek request"); + } else { + position = (int) pos; + } + } + + @Override + public long length() { + return data.length; + } + + @Override + public long getFilePointer() { + return position; + } + + @Override + public void close() { + // do nothing + } + + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/ReadWriteOakDirectory.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/ReadWriteOakDirectory.java new file mode 100644 index 00000000000..65c91e50fcb --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/ReadWriteOakDirectory.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.lucene; + +import java.io.IOException; + +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.IOContext; +import org.apache.lucene.store.IndexOutput; + +/** + * A red-write implementation of the Lucene {@link Directory} (a flat list of + * files) that allows to store Lucene index content in an Oak repository. + */ +public class ReadWriteOakDirectory extends ReadOnlyOakDirectory { + + public ReadWriteOakDirectory(NodeBuilder directoryBuilder) { + super(directoryBuilder); + } + + @Override + public IndexOutput createOutput(String name, IOContext context) + throws IOException { + return new OakIndexOutput(name); + } + + private final class OakIndexOutput extends IndexOutput { + + private final String name; + + private byte[] buffer; + + private int size; + + private int position; + + public OakIndexOutput(String name) throws IOException { + this.name = name; + this.buffer = readFile(name); + this.size = buffer.length; + this.position = 0; + } + + @Override + public long length() { + return size; + } + + @Override + public long getFilePointer() { + return position; + } + + @Override + public void seek(long pos) throws IOException { + if (pos < 0 || pos > Integer.MAX_VALUE) { + throw new IOException("Invalid file position: " + pos); + } + this.position = (int) pos; + } + + @Override + public void writeBytes(byte[] b, int offset, int length) { + while (position + length > buffer.length) { + byte[] tmp = new byte[Math.max(4096, buffer.length * 2)]; + System.arraycopy(buffer, 0, tmp, 0, size); + buffer = tmp; + } + + System.arraycopy(b, offset, buffer, position, length); + + position += length; + if (position > size) { + size = position; + } + } + + @Override + public void writeByte(byte b) { + writeBytes(new byte[] { b }, 0, 1); + } + + @Override + public void flush() throws IOException { + byte[] data = buffer; + if (data.length > size) { + data = new byte[size]; + System.arraycopy(buffer, 0, data, 0, size); + } + + NodeBuilder fileBuilder = directoryBuilder.child(name); + fileBuilder.setProperty("jcr:lastModified", System.currentTimeMillis()); + fileBuilder.setProperty("jcr:data", data); + } + + @Override + public void close() throws IOException { + flush(); + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/TermFactory.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/TermFactory.java new file mode 100644 index 00000000000..7dd3f29b285 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/TermFactory.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.lucene; + +import org.apache.lucene.index.Term; + +/** + * {@code TermFactory} is a factory for Term instances with + * frequently used field names. + */ +public final class TermFactory { + + /** + * Private constructor. + */ + private TermFactory() { + } + + /** + * Creates a Term with the given {@code path} value and with a field + * name {@link FieldNames#PATH}. + * + * @param path + * the path. + * @return the path term. + */ + public static Term newPathTerm(String path) { + return new Term(FieldNames.PATH, path); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/index/BTree.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/BTree.java similarity index 91% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/index/BTree.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/BTree.java index f2ff8f25f08..1bbdde357c2 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/index/BTree.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/BTree.java @@ -14,34 +14,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.index; +package org.apache.jackrabbit.oak.plugins.index.old; import org.apache.jackrabbit.mk.json.JsopBuilder; -import org.apache.jackrabbit.mk.util.PathUtils; +import org.apache.jackrabbit.oak.commons.PathUtils; /** * A tree allows to query a value for a given key, similar to - * java.util.SortedMap. + * {@code java.util.SortedMap}. */ -public class BTree { +public class BTree implements PropertyIndexConstants { private static final int DEFAULT_MIN_SIZE = 2; - private Indexer indexer; + private BTreeHelper indexer; private String name; private boolean unique = true; private int minSize = DEFAULT_MIN_SIZE; - public BTree(Indexer indexer, String name, boolean unique) { + public BTree(BTreeHelper indexer, String name, boolean unique) { this.indexer = indexer; this.name = name; this.unique = unique; - if (!indexer.nodeExists(name)) { - JsopBuilder jsop = new JsopBuilder(); - jsop.tag('+').key(name).object().endObject(); - indexer.commit(jsop.toString()); - } + indexer.createNodes(PathUtils.concat(name, INDEX_CONTENT)); } public void setMinSize(int minSize) { @@ -127,9 +123,9 @@ public Cursor findFirst(String key) { void bufferSetArray(String path, String propertyName, String[] data) { JsopBuilder jsop = new JsopBuilder(); - path = PathUtils.concat(name, path); + path = PathUtils.concat(name, INDEX_CONTENT, path); jsop.tag('^').key(PathUtils.concat(path, propertyName)); - if (data.length == 0) { + if (data == null) { jsop.value(null); } else { jsop.array(); @@ -151,7 +147,7 @@ void bufferMove(String path, String newPath) { void bufferDelete(String path) { JsopBuilder jsop = new JsopBuilder(); - jsop.tag('-').value(PathUtils.concat(name, path)); + jsop.tag('-').value(PathUtils.concat(name, INDEX_CONTENT, path)); jsop.newline(); indexer.buffer(jsop.toString()); } @@ -164,7 +160,7 @@ void modified(BTreePage page) { indexer.modified(this, page, false); } - public void moveCache(String oldPath) { + void moveCache(String oldPath) { indexer.moveCache(this, oldPath); } @@ -208,7 +204,6 @@ public boolean remove(String key, String value) { break; } } - commit(); return true; } @@ -240,8 +235,6 @@ public void add(String key, String value) { // other node now) n = parent; } - // subsequent operations are based on the new structure - commit(); } if (n instanceof BTreeNode) { BTreeNode page = (BTreeNode) n; @@ -269,14 +262,9 @@ public void add(String key, String value) { break; } } - commit(); - } - - void commit() { - indexer.commit(); } - String getName() { + public String getName() { return name; } @@ -288,4 +276,8 @@ private int getMaxSize() { return minSize + minSize + 1; } + boolean isUnique() { + return unique; + } + } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/BTreeHelper.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/BTreeHelper.java new file mode 100644 index 00000000000..dcabf660f2f --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/BTreeHelper.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.plugins.index.old; + +public interface BTreeHelper { + + BTreePage getPageIfCached(BTree tree, BTreeNode parent, String name); + + BTreePage getPage(BTree tree, BTreeNode parent, String name); + + void modified(BTree tree, BTreePage page, boolean deleted); + + void moveCache(BTree tree, String oldPath); + + void createNodes(String path); + + void buffer(String diff); + + void updateUntil(String toRevision); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/index/BTreeLeaf.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/BTreeLeaf.java similarity index 70% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/index/BTreeLeaf.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/BTreeLeaf.java index d0dfedb44d1..c1db1e150f1 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/index/BTreeLeaf.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/BTreeLeaf.java @@ -14,30 +14,34 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.index; +package org.apache.jackrabbit.oak.plugins.index.old; import java.util.Arrays; + import org.apache.jackrabbit.mk.json.JsopBuilder; -import org.apache.jackrabbit.mk.util.ArrayUtils; -import org.apache.jackrabbit.mk.util.PathUtils; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.util.ArrayUtils; /** * An index leaf page. */ -class BTreeLeaf extends BTreePage { +public class BTreeLeaf extends BTreePage implements PropertyIndexConstants { - BTreeLeaf(BTree tree, BTreeNode parent, String name, String[] data, String[] paths) { + public BTreeLeaf(BTree tree, BTreeNode parent, String name, String[] data, String[] paths) { super(tree, parent, name, data, paths); + verify(); } BTreeLeaf nextLeaf() { return parent == null ? null : parent.next(this); } + @Override BTreeLeaf firstLeaf() { return this; } + @Override void split(BTreeNode newParent, String newName, int pos, String siblingName) { setParent(newParent, newName, true); String[] k2 = Arrays.copyOfRange(keys, pos, keys.length, String[].class); @@ -53,24 +57,42 @@ void insert(int pos, String key, String value) { tree.modified(this); keys = ArrayUtils.arrayInsert(keys, pos, key); values = ArrayUtils.arrayInsert(values, pos, value); + verify(); } void delete(int pos) { tree.modified(this); keys = ArrayUtils.arrayRemove(keys, pos); values = ArrayUtils.arrayRemove(values, pos); + verify(); } void writeData() { + verify(); tree.modified(this); + tree.bufferSetArray(getPath(), "children", null); tree.bufferSetArray(getPath(), "keys", keys); tree.bufferSetArray(getPath(), "values", values); } + @Override void writeCreate() { + verify(); tree.modified(this); + tree.buffer(getJsop()); + } + + private void verify() { + if (values.length != keys.length) { + throw new IllegalArgumentException( + "Number of values doesn't match number of keys: " + + Arrays.toString(values) + " " + Arrays.toString(keys)); + } + } + + private String getJsop() { JsopBuilder jsop = new JsopBuilder(); - jsop.tag('+').key(PathUtils.concat(tree.getName(), getPath())).object(); + jsop.tag('+').key(PathUtils.concat(tree.getName(), INDEX_CONTENT, getPath())).object(); jsop.key("keys").array(); for (String k : keys) { jsop.value(k); @@ -83,7 +105,12 @@ void writeCreate() { jsop.endArray(); jsop.endObject(); jsop.newline(); - tree.buffer(jsop.toString()); + return jsop.toString(); + } + + @Override + public String toString() { + return "leaf: " + getJsop(); } } \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/index/BTreeNode.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/BTreeNode.java similarity index 73% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/index/BTreeNode.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/BTreeNode.java index ea4e1f5ab10..c2d2c3379b6 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/index/BTreeNode.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/BTreeNode.java @@ -14,23 +14,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.index; +package org.apache.jackrabbit.oak.plugins.index.old; import java.util.Arrays; + import org.apache.jackrabbit.mk.json.JsopBuilder; -import org.apache.jackrabbit.mk.util.ArrayUtils; -import org.apache.jackrabbit.mk.util.PathUtils; +import org.apache.jackrabbit.oak.util.ArrayUtils; +import org.apache.jackrabbit.oak.commons.PathUtils; /** * An index node page. */ -class BTreeNode extends BTreePage { +public class BTreeNode extends BTreePage implements PropertyIndexConstants { private String[] children; - BTreeNode(BTree tree, BTreeNode parent, String name, String[] keys, String[] values, String[] children) { + public BTreeNode(BTree tree, BTreeNode parent, String name, String[] keys, String[] values, String[] children) { super(tree, parent, name, keys, values); this.children = children; + verify(); } String getNextChildPath() { @@ -44,10 +46,12 @@ String getNextChildPath() { return Integer.toString(max + 1); } + @Override BTreeLeaf firstLeaf() { return tree.getPage(this, children[0]).firstLeaf(); } + @Override void split(BTreeNode newParent, String newName, int pos, String siblingName) { setParent(newParent, newName, true); String[] k2 = Arrays.copyOfRange(keys, pos + 1, keys.length, String[].class); @@ -63,11 +67,12 @@ void split(BTreeNode newParent, String newName, int pos, String siblingName) { keys = Arrays.copyOfRange(keys, 0, pos, String[].class); values = Arrays.copyOfRange(values, 0, pos, String[].class); children = Arrays.copyOfRange(children, 0, pos + 1, String[].class); + verify(); n2.writeCreate(); for (String c : n2.children) { tree.bufferMove( - PathUtils.concat(tree.getName(), getPath(), c), - PathUtils.concat(tree.getName(), getParentPath(), siblingName, c) + PathUtils.concat(tree.getName(), INDEX_CONTENT, getPath(), c), + PathUtils.concat(tree.getName(), INDEX_CONTENT, getParentPath(), siblingName, c) ); } tree.moveCache(getPath()); @@ -93,16 +98,61 @@ BTreePage getChild(int pos) { } void writeData() { + verify(); tree.modified(this); tree.bufferSetArray(getPath(), "keys", keys); tree.bufferSetArray(getPath(), "values", values); tree.bufferSetArray(getPath(), "children", children); } + @Override void writeCreate() { + verify(); + tree.modified(this); + tree.buffer(getJsop()); + } + + void delete(int pos) { + tree.modified(this); + if (size() > 0) { + // empty parent + keys = ArrayUtils.arrayRemove(keys, Math.max(0, pos - 1)); + values = ArrayUtils.arrayRemove(values, Math.max(0, pos - 1)); + } + children = ArrayUtils.arrayRemove(children, pos); + verify(); + } + + void insert(int pos, String key, String value, String child) { tree.modified(this); + keys = ArrayUtils.arrayInsert(keys, pos, key); + values = ArrayUtils.arrayInsert(values, pos, value); + children = ArrayUtils.arrayInsert(children, pos + 1, child); + verify(); + } + + boolean isEmpty() { + return children.length == 0; + } + + private void verify() { + if (values.length != keys.length) { + throw new IllegalArgumentException( + "Number of values doesn't match number of keys: " + + Arrays.toString(values) + " " + Arrays.toString(keys) + " " + Arrays.toString(children)); + } + if (children.length != keys.length + 1) { + if (children.length != 0 || keys.length != 0) { + throw new IllegalArgumentException( + "Number of children doesn't match number of keys + 1: " + + Arrays.toString(values) + " " + Arrays.toString(keys) + " " + Arrays.toString(children)); + } + } + } + + private String getJsop() { JsopBuilder jsop = new JsopBuilder(); - jsop.tag('+').key(PathUtils.concat(tree.getName(), getPath())).object(); + jsop.tag('+').key(PathUtils.concat(tree.getName(), INDEX_CONTENT, getPath())).object(); jsop.key("keys").array(); for (String k : keys) { jsop.value(k); @@ -124,28 +174,12 @@ void writeCreate() { jsop.endArray(); jsop.endObject(); jsop.newline(); - tree.buffer(jsop.toString()); - } - - void delete(int pos) { - tree.modified(this); - if (size() > 0) { - // empty parent - keys = ArrayUtils.arrayRemove(keys, Math.max(0, pos - 1)); - values = ArrayUtils.arrayRemove(values, Math.max(0, pos - 1)); - } - children = ArrayUtils.arrayRemove(children, pos); - } - - void insert(int pos, String key, String value, String child) { - tree.modified(this); - keys = ArrayUtils.arrayInsert(keys, pos, key); - values = ArrayUtils.arrayInsert(values, pos, value); - children = ArrayUtils.arrayInsert(children, pos + 1, child); + return jsop.toString(); } - boolean isEmpty() { - return children.length == 0; + @Override + public String toString() { + return "node: " + getJsop(); } } \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/index/BTreePage.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/BTreePage.java similarity index 81% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/index/BTreePage.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/BTreePage.java index f8f5e914b96..3966e3a84bf 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/index/BTreePage.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/BTreePage.java @@ -14,14 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.index; +package org.apache.jackrabbit.oak.plugins.index.old; -import org.apache.jackrabbit.mk.util.PathUtils; +import org.apache.jackrabbit.oak.commons.PathUtils; /** * An index page. */ -abstract class BTreePage { +public abstract class BTreePage implements PropertyIndexConstants { protected final BTree tree; protected BTreeNode parent; @@ -44,20 +44,20 @@ abstract class BTreePage { void setParent(BTreeNode newParent, String newName, boolean parentIsNew) { if (newParent != null) { String oldPath = getPath(); + String temp = PathUtils.concat(INDEX_CONTENT, "temp"); tree.bufferMove( - PathUtils.concat(tree.getName(), getPath()), - "temp"); + PathUtils.concat(tree.getName(), INDEX_CONTENT, getPath()), + temp); if (parentIsNew) { newParent.writeCreate(); } tree.bufferMove( - "temp", - PathUtils.concat(tree.getName(), getParentPath(), newName)); + temp, + PathUtils.concat(tree.getName(), INDEX_CONTENT, getParentPath(), newName)); parent = newParent; name = newName; tree.moveCache(oldPath); tree.modified(this); - tree.commit(); } } @@ -65,7 +65,7 @@ String getParentPath() { return parent == null ? "" : parent.getPath(); } - String getPath() { + public String getPath() { return PathUtils.concat(getParentPath(), name); } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/index/Cursor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/Cursor.java similarity index 93% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/index/Cursor.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/Cursor.java index 26b3da3ff9d..24b889b1f71 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/index/Cursor.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/Cursor.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.index; +package org.apache.jackrabbit.oak.plugins.index.old; import java.util.Iterator; @@ -28,10 +28,12 @@ public class Cursor implements Iterator { private int pos; private String currentValue; + @Override public boolean hasNext() { return current != null; } + @Override public String next() { if (current == null) { currentValue = null; @@ -61,6 +63,7 @@ void step() { } } + @Override public void remove() { throw new UnsupportedOperationException(); } @@ -70,15 +73,20 @@ void setCurrent(BTreeLeaf current, int pos) { this.pos = pos; } + /** + * An iterator over a cursor. + */ public static class RangeIterator implements Iterator { private final Cursor cursor; private final String maxKey; private String value; + RangeIterator(Cursor cursor, String maxKey) { this.cursor = cursor; this.maxKey = maxKey; step(); } + private void step() { value = null; if (cursor.hasNext()) { @@ -88,17 +96,24 @@ private void step() { } } } + + @Override public boolean hasNext() { return value != null; } + + @Override public String next() { String v = value; step(); return v; } + + @Override public void remove() { throw new UnsupportedOperationException(); } + } } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/index/Indexer.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/Indexer.java similarity index 57% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/index/Indexer.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/Indexer.java index 68a15244c33..5630f203b02 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/index/Indexer.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/Indexer.java @@ -14,102 +14,165 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.index; +package org.apache.jackrabbit.oak.plugins.index.old; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map.Entry; -import org.apache.jackrabbit.mk.MicroKernelImpl; import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.api.MicroKernelException; import org.apache.jackrabbit.mk.json.JsopBuilder; import org.apache.jackrabbit.mk.json.JsopReader; import org.apache.jackrabbit.mk.json.JsopTokenizer; -import org.apache.jackrabbit.mk.simple.NodeImpl; -import org.apache.jackrabbit.mk.simple.NodeMap; -import org.apache.jackrabbit.mk.util.ExceptionFactory; -import org.apache.jackrabbit.mk.util.PathUtils; import org.apache.jackrabbit.mk.util.SimpleLRUCache; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map.Entry; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.plugins.index.old.mk.ExceptionFactory; +import org.apache.jackrabbit.oak.plugins.index.old.mk.IndexWrapper; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeImpl; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeMap; /** * A index mechanism. An index is bound to a certain repository, and supports * one or more indexes. + * + * @deprecated use {@link PropertyIndexer} - see OAK-298 */ -public class Indexer { +public class Indexer implements PropertyIndexConstants, BTreeHelper { + + /** + * The maximum length of the write buffer. + */ + private static final int MAX_BUFFER_LENGTH = 100000; private static final boolean DISABLED = Boolean.getBoolean("mk.indexDisabled"); private MicroKernel mk; private String revision; - private String indexRootNode; + private final String indexRootNode; + private final int indexRootNodeDepth; private StringBuilder buffer; - private HashMap indexes = new HashMap(); private HashMap modified = new HashMap(); private SimpleLRUCache cache = SimpleLRUCache.newInstance(100); private String readRevision; + private boolean init; + + /** + * An index node name to index map. + */ + private HashMap indexes = new HashMap(); + + /** + * A prefix to prefix index map. + */ + private final HashMap prefixIndexes = new HashMap(); + + /** + * A property name to property index map. + */ + private final HashMap propertyIndexes = new HashMap(); - public Indexer(MicroKernel mk, String indexRootNode) { + public Indexer(MicroKernel mk, String indexConfigPath) { this.mk = mk; - if (!PathUtils.isAbsolute(indexRootNode)) { - indexRootNode = "/" + indexRootNode; + this.indexRootNode = indexConfigPath; + this.indexRootNodeDepth = PathUtils.getDepth(indexRootNode); + } + + /** + * TODO test-only + */ + public Indexer(MicroKernel mk) { + this(mk, INDEX_CONFIG_PATH); + } + + /** + * Get the indexer for the given MicroKernel. This will either create a new instance, + * or (if the MicroKernel is an IndexWrapper), return the existing indexer. + * + * @param mk the MicroKernel instance + * @return an indexer + */ + public static Indexer getInstance(MicroKernel mk) { + if (mk instanceof IndexWrapper) { + Indexer indexer = ((IndexWrapper) mk).getIndexer(); + if (indexer != null) { + return indexer; + } + } + return new Indexer(mk); + } + + public String getIndexRootNode() { + return indexRootNode; + } + + public void init() { + if (init) { + return; } - this.indexRootNode = indexRootNode; + init = true; revision = mk.getHeadRevision(); readRevision = revision; - if (!mk.nodeExists(indexRootNode, revision)) { - JsopBuilder jsop = new JsopBuilder(); - jsop.tag('+').key(PathUtils.relativize("/", indexRootNode)).object().endObject(); - revision = mk.commit("/", jsop.toString(), revision, null); - } else { - String node = mk.getNodes(indexRootNode, revision, 0, 0, Integer.MAX_VALUE, null); + boolean exists = mk.nodeExists(indexRootNode, revision); + createNodes(INDEX_CONTENT); + if (exists) { + String node = mk.getNodes(indexRootNode, revision, 1, 0, Integer.MAX_VALUE, null); JsopTokenizer t = new JsopTokenizer(node); + NodeMap map = new NodeMap(); t.read('{'); - HashMap map = new HashMap(); - do { - String key = t.readString(); - t.read(':'); - t.read(); - String value = t.getToken(); - map.put(key, value); - } while (t.matches(',')); - String rev = map.get("rev"); + NodeImpl n = NodeImpl.parse(map, t, 0); + String rev = JsopTokenizer.decodeQuoted(n.getNode(INDEX_CONTENT).getProperty("rev")); if (rev != null) { readRevision = rev; } - for (String k : map.keySet()) { - Index p = PropertyIndex.fromNodeName(this, k); - if (p == null) { - p = PrefixIndex.fromNodeName(this, k); + for (int i = 0; i < n.getChildNodeCount(); i++) { + String k = n.getChildNodeName(i); + PropertyIndex prop = PropertyIndex.fromNodeName(this, k); + if (prop != null) { + indexes.put(prop.getIndexNodeName(), prop); + propertyIndexes.put(prop.getPropertyName(), prop); } - if (p != null) { - indexes.put(p.getName(), p); + PrefixIndex pref = PrefixIndex.fromNodeName(this, k); + if (pref != null) { + indexes.put(pref.getIndexNodeName(), pref); + prefixIndexes.put(pref.getPrefix(), pref); } } } } - public Indexer(MicroKernel mk) { - this(mk, "/index"); + private void removePropertyIndex(String property, boolean unique) { + PropertyIndex index = propertyIndexes.remove(property); + indexes.remove(index.getIndexNodeName()); } public PropertyIndex createPropertyIndex(String property, boolean unique) { - PropertyIndex index = new PropertyIndex(this, property, unique); - PropertyIndex existing = (PropertyIndex) indexes.get(index.getName()); + PropertyIndex existing = propertyIndexes.get(property); if (existing != null) { return existing; } - buildAndAddIndex(index); + PropertyIndex index = new PropertyIndex(this, property, unique); + buildIndex(index); + indexes.put(index.getIndexNodeName(), index); + propertyIndexes.put(index.getPropertyName(), index); return index; } + private void removePrefixIndex(String prefix) { + PrefixIndex index = prefixIndexes.remove(prefix); + indexes.remove(index.getIndexNodeName()); + } + public PrefixIndex createPrefixIndex(String prefix) { - PrefixIndex index = new PrefixIndex(this, prefix); - PrefixIndex existing = (PrefixIndex) indexes.get(index.getName()); + PrefixIndex existing = prefixIndexes.get(prefix); if (existing != null) { return existing; } - buildAndAddIndex(index); + PrefixIndex index = new PrefixIndex(this, prefix); + buildIndex(index); + indexes.put(index.getIndexNodeName(), index); + prefixIndexes.put(index.getPrefix(), index); return index; } @@ -118,22 +181,37 @@ boolean nodeExists(String name) { return mk.nodeExists(PathUtils.concat(indexRootNode, name), revision); } + public void createNodes(String path) { + String rev = mk.getHeadRevision(); + JsopBuilder jsop = new JsopBuilder(); + String p = "/"; + path = PathUtils.concat(indexRootNode, path); + for (String e : PathUtils.elements(path)) { + p = PathUtils.concat(p, e); + if (!mk.nodeExists(p, rev)) { + jsop.tag('+').key(PathUtils.relativize("/", p)). + object().endObject().newline(); + } + } + revision = mk.commit("/", jsop.toString(), rev, null); + } + void commit(String jsop) { revision = mk.commit(indexRootNode, jsop, revision, null); } - BTreePage getPageIfCached(BTree tree, BTreeNode parent, String name) { + public BTreePage getPageIfCached(BTree tree, BTreeNode parent, String name) { String p = getPath(tree, parent, name); return modified.get(p); } private String getPath(BTree tree, BTreeNode parent, String name) { String p = parent == null ? name : PathUtils.concat(parent.getPath(), name); - String indexRoot = PathUtils.concat(indexRootNode, tree.getName()); + String indexRoot = PathUtils.concat(indexRootNode, tree.getName(), INDEX_CONTENT); return PathUtils.concat(indexRoot, p); } - BTreePage getPage(BTree tree, BTreeNode parent, String name) { + public BTreePage getPage(BTree tree, BTreeNode parent, String name) { String p = getPath(tree, parent, name); BTreePage page; page = modified.get(p); @@ -186,24 +264,17 @@ private static String[] readArray(String json) { return data; } - void buffer(String diff) { + public void buffer(String diff) { if (buffer == null) { - buffer = new StringBuilder(diff.length()); - } - buffer.append(diff); - } - - void commit() { - // TODO remove this method once MicroKernelImpl supports - // move + add node - if (mk instanceof MicroKernelImpl) { - commitChanges(); + buffer = new StringBuilder(diff); + } else { + buffer.append(diff); } } - void modified(BTree tree, BTreePage page, boolean deleted) { + public void modified(BTree tree, BTreePage page, boolean deleted) { String indexRoot = PathUtils.concat(indexRootNode, tree.getName()); - String p = PathUtils.concat(indexRoot, page.getPath()); + String p = PathUtils.concat(indexRoot, INDEX_CONTENT, page.getPath()); if (deleted) { modified.remove(p); } else { @@ -213,7 +284,7 @@ void modified(BTree tree, BTreePage page, boolean deleted) { public void moveCache(BTree tree, String oldPath) { String indexRoot = PathUtils.concat(indexRootNode, tree.getName()); - String o = PathUtils.concat(indexRoot, oldPath); + String o = PathUtils.concat(indexRoot, INDEX_CONTENT, oldPath); HashMap moved = new HashMap(); for (Entry e : modified.entrySet()) { if (e.getKey().startsWith(o)) { @@ -224,12 +295,12 @@ public void moveCache(BTree tree, String oldPath) { modified.remove(s); } for (BTreePage p : moved.values()) { - String n = PathUtils.concat(indexRoot, p.getPath()); + String n = PathUtils.concat(indexRoot, INDEX_CONTENT, p.getPath()); modified.put(n, p); } } - void commitChanges() { + private void commitChanges() { if (buffer != null) { String jsop = buffer.toString(); // System.out.println(jsop); @@ -239,7 +310,7 @@ void commitChanges() { } } - synchronized void updateUntil(String toRevision) { + public synchronized void updateUntil(String toRevision) { if (DISABLED) { return; } @@ -277,6 +348,9 @@ synchronized void updateUntil(String toRevision) { } lastRevision = rev; t.read('}'); + if (buffer != null && buffer.length() > MAX_BUFFER_LENGTH) { + updateEnd(rev); + } } while (t.matches(',')); updateEnd(toRevision); } @@ -290,25 +364,42 @@ synchronized void updateUntil(String toRevision) { public String updateEnd(String toRevision) { readRevision = toRevision; JsopBuilder jsop = new JsopBuilder(); - jsop.tag('^').key("rev").value(readRevision); + jsop.tag('^').key(PathUtils.concat(INDEX_CONTENT, "rev")).value(readRevision); buffer(jsop.toString()); - commitChanges(); + flushBuffer(); return revision; } + private void flushBuffer() { + try { + commitChanges(); + } catch (MicroKernelException e) { + if (!mk.nodeExists(indexRootNode, revision)) { + // the index node itself was removed, which is + // unexpected but possible + // this will cause all indexes to be removed, so + // it can be ignored here + } else { + throw e; + } + } + } + /** * Update the index with the given changes. * + * @param rootPath the root path * @param t the changes * @param lastRevision */ public void updateIndex(String rootPath, JsopReader t, String lastRevision) { while (true) { int r = t.read(); - if (r == JsopTokenizer.END) { + if (r == JsopReader.END) { break; } String path = PathUtils.concat(rootPath, t.readString()); + String target; switch (r) { case '+': { t.read(':'); @@ -327,13 +418,21 @@ public void updateIndex(String rootPath, JsopReader t, String lastRevision) { } break; } + case '*': + // TODO support and test copy operation ("*"), + // specially in combination with other operations + // possibly split up the commit in this case + t.read(':'); + target = t.readString(); + moveOrCopyNode(path, false, target, lastRevision); + break; case '-': - moveNode(path, null, lastRevision); + moveOrCopyNode(path, true, null, lastRevision); break; case '^': { removeProperty(path, lastRevision); t.read(':'); - if (t.matches(JsopTokenizer.NULL)) { + if (t.matches(JsopReader.NULL)) { // ignore } else { String value = t.readRawValue().trim(); @@ -342,9 +441,12 @@ public void updateIndex(String rootPath, JsopReader t, String lastRevision) { break; } case '>': + // TODO does move work correctly + // in combination with other operations? + // possibly split up the commit in this case t.read(':'); String name = PathUtils.getName(path); - String target, position; + String position; if (t.matches('{')) { position = t.readString(); t.read(':'); @@ -364,7 +466,7 @@ public void updateIndex(String rootPath, JsopReader t, String lastRevision) { } else { throw ExceptionFactory.get("position: " + position); } - moveNode(path, target, lastRevision); + moveOrCopyNode(path, true, target, lastRevision); break; default: throw new AssertionError("token: " + (char) t.getTokenType()); @@ -373,11 +475,13 @@ public void updateIndex(String rootPath, JsopReader t, String lastRevision) { } private void addOrRemoveRecursive(NodeImpl n, boolean remove, boolean add) { - if (isInIndex(n.getPath())) { - // don't index the index + String path = n.getPath(); + if (isInIndex(path)) { + addOrRemoveIndex(path, remove, add); + // don't index the index data itself return; } - for (Index index : indexes.values()) { + for (PIndex index : indexes.values()) { if (remove) { index.addOrRemoveNode(n, false); } @@ -390,13 +494,53 @@ private void addOrRemoveRecursive(NodeImpl n, boolean remove, boolean add) { } } + private void addOrRemoveIndex(String path, boolean remove, boolean add) { + // check the depth first for speed + + // TODO allow creating multiple indexes in one step + // (buffer indexes to be created; traverse the repository only once) + // TODO allow filters (only index a certain path; exclude a list of paths) + + if (PathUtils.getDepth(path) == indexRootNodeDepth + 1) { + // actually not required, just to make sure + if (PathUtils.getParentPath(path).equals(indexRootNode)) { + String name = PathUtils.getName(path); + if (name.startsWith(PropertyIndexConstants.TYPE_PREFIX)) { + String prefix = name.substring(PropertyIndexConstants.TYPE_PREFIX.length()); + if (remove) { + removePrefixIndex(prefix); + } + if (add) { + createPrefixIndex(prefix); + } + } else if (name.startsWith(PropertyIndexConstants.TYPE_PROPERTY)) { + String property = name.substring(PropertyIndexConstants.TYPE_PROPERTY.length()); + boolean unique = false; + if (property.endsWith("," + PropertyIndexConstants.UNIQUE)) { + unique = true; + property = property.substring(0, property.length() - PropertyIndexConstants.UNIQUE.length() - 1); + } + if (remove) { + removePropertyIndex(property, unique); + } + if (add) { + createPropertyIndex(property, unique); + } + } + } + } + } + private boolean isInIndex(String path) { - return PathUtils.isAncestor(indexRootNode, path) || indexRootNode.equals(path); + if (PathUtils.isAncestor(indexRootNode, path) || indexRootNode.equals(path)) { + return true; + } + return false; } private void removeProperty(String path, String lastRevision) { if (isInIndex(path)) { - // don't index the index + // don't index the index data itself return; } String nodePath = PathUtils.getParentPath(path); @@ -412,7 +556,7 @@ private void removeProperty(String path, String lastRevision) { NodeImpl n = NodeImpl.parse(map, t, 0, path); if (n.hasProperty(property)) { n.setPath(nodePath); - for (Index index : indexes.values()) { + for (PIndex index : indexes.values()) { index.addOrRemoveProperty(nodePath, property, n.getProperty(property), false); } } @@ -420,19 +564,25 @@ private void removeProperty(String path, String lastRevision) { private void addProperty(String path, String value) { if (isInIndex(path)) { - // don't index the index + // don't index the index data itself return; } String nodePath = PathUtils.getParentPath(path); String property = PathUtils.getName(path); - for (Index index : indexes.values()) { + for (PIndex index : indexes.values()) { index.addOrRemoveProperty(nodePath, property, value, true); } } - private void moveNode(String sourcePath, String targetPath, String lastRevision) { + private void moveOrCopyNode(String sourcePath, boolean remove, String targetPath, String lastRevision) { if (isInIndex(sourcePath)) { - // don't index the index + if (remove) { + addOrRemoveIndex(sourcePath, true, false); + } + if (targetPath != null) { + addOrRemoveIndex(targetPath, false, true); + } + // don't index the index data itself return; } if (!mk.nodeExists(sourcePath, lastRevision)) { @@ -444,7 +594,9 @@ private void moveNode(String sourcePath, String targetPath, String lastRevision) NodeMap map = new NodeMap(); t.read('{'); NodeImpl n = NodeImpl.parse(map, t, 0, sourcePath); - addOrRemoveRecursive(n, true, false); + if (remove) { + addOrRemoveRecursive(n, true, false); + } if (targetPath != null) { t = new JsopTokenizer(node); map = new NodeMap(); @@ -454,12 +606,12 @@ private void moveNode(String sourcePath, String targetPath, String lastRevision) } } - private void buildAndAddIndex(Index index) { + private void buildIndex(PIndex index) { + // TODO index: add ability to start / stop / restart indexing; log the progress addRecursive(index, "/"); - indexes.put(index.getName(), index); } - private void addRecursive(Index index, String path) { + private void addRecursive(PIndex index, String path) { if (isInIndex(path)) { return; } @@ -473,6 +625,21 @@ private void addRecursive(Index index, String path) { for (Iterator it = n.getChildNodeNames(Integer.MAX_VALUE); it.hasNext();) { addRecursive(index, PathUtils.concat(path, it.next())); } + if (needFlush()) { + flushBuffer(); + } + } + + private boolean needFlush() { + return buffer != null && buffer.length() > MAX_BUFFER_LENGTH; + } + + public PrefixIndex getPrefixIndex(String prefix) { + return prefixIndexes.get(prefix); + } + + public PropertyIndex getPropertyIndex(String property) { + return propertyIndexes.get(property); } } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/index/Index.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PIndex.java similarity index 69% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/index/Index.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PIndex.java index 96b2b68f724..68749c625b4 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/index/Index.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PIndex.java @@ -14,23 +14,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.index; +package org.apache.jackrabbit.oak.plugins.index.old; -import org.apache.jackrabbit.mk.simple.NodeImpl; +import java.util.Iterator; + +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeImpl; /** * An index is a lookup mechanism. It typically uses a tree to store data. It * updates the tree whenever a node was changed. The index is updated * automatically. */ -public interface Index { +public interface PIndex { /** * Get the unique index name. This is also the name of the index node. * * @return the index name */ - String getName(); + String getIndexNodeName(); /** * The given node was added or removed. @@ -51,4 +53,21 @@ public interface Index { void addOrRemoveProperty(String nodePath, String propertyName, String value, boolean add); + /** + * Get an iterator over the paths for the given value. For unique + * indexes, the iterator will contain at most one element. + * + * @param value the value, or null to return all indexed rows + * @param revision the revision + * @return an iterator of the paths (an empty iterator if not found) + */ + Iterator getPaths(String value, String revision); + + /** + * Whether each value may only appear once in the index. + * + * @return true if unique + */ + boolean isUnique(); + } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PrefixContentIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PrefixContentIndex.java new file mode 100644 index 00000000000..18436490c87 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PrefixContentIndex.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.old; + +import java.util.Iterator; + +import javax.jcr.PropertyType; + +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.kernel.TypeCodes; +import org.apache.jackrabbit.oak.query.index.IndexRowImpl; +import org.apache.jackrabbit.oak.spi.query.Cursor; +import org.apache.jackrabbit.oak.spi.query.Filter; +import org.apache.jackrabbit.oak.spi.query.IndexRow; +import org.apache.jackrabbit.oak.spi.query.QueryIndex; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * An index that stores the index data in a {@code MicroKernel}. + * + * @deprecated the revisionId info has been removed + */ +public class PrefixContentIndex implements QueryIndex { + + private final PrefixIndex index; + + public PrefixContentIndex(PrefixIndex index) { + this.index = index; + } + + @Override + public double getCost(Filter filter, NodeState root) { + if (getPropertyTypeRestriction(filter) != null) { + return 100; + } + return Double.MAX_VALUE; + } + + private Filter.PropertyRestriction getPropertyTypeRestriction(Filter filter) { + for (Filter.PropertyRestriction restriction : filter.getPropertyRestrictions()) { + if (restriction == null) { + continue; + } + if (restriction.first != restriction.last) { + // only support equality matches (for now) + continue; + } + if (restriction.propertyType == PropertyType.UNDEFINED) { + continue; + } + String code = TypeCodes.getCodeForType(restriction.propertyType); + String prefix = code + ":"; + if (prefix.equals(index.getPrefix())) { + return restriction; + } + } + return null; + } + + @Override + public String getPlan(Filter filter, NodeState root) { + Filter.PropertyRestriction restriction = getPropertyTypeRestriction(filter); + if (restriction == null) { + throw new IllegalArgumentException("No restriction for *"); + } + // TODO need to use correct json representation + String v = restriction.first.getValue(Type.STRING); + v = index.getPrefix() + v; + return "prefixIndex \"" + v + '"'; + } + + @Override + public Cursor query(Filter filter, NodeState root) { + Filter.PropertyRestriction restriction = getPropertyTypeRestriction(filter); + if (restriction == null) { + throw new IllegalArgumentException("No restriction for *"); + } + // TODO need to use correct json representation + String v = restriction.first.getValue(Type.STRING); + v = index.getPrefix() + v; + // TODO revisit code after the removal of revisionId + String revisionId = ""; + Iterator it = index.getPaths(v, revisionId); + return new ContentCursor(it); + } + + @Override + public String getIndexName() { + return index.getIndexNodeName(); + } + + /** + * The cursor to for this index. + */ + static class ContentCursor implements Cursor { + + private final Iterator it; + + private String currentPath; + + public ContentCursor(Iterator it) { + this.it = it; + } + + @Override + public IndexRow currentRow() { + return new IndexRowImpl(currentPath); + } + + @Override + public boolean next() { + if (it.hasNext()) { + String pathAndProperty = it.next(); + currentPath = PathUtils.getParentPath(pathAndProperty); + return true; + } + return false; + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/index/PrefixIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PrefixIndex.java similarity index 71% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/index/PrefixIndex.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PrefixIndex.java index 8b64f094850..dae54760c44 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/index/PrefixIndex.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PrefixIndex.java @@ -14,40 +14,44 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.index; +package org.apache.jackrabbit.oak.plugins.index.old; import java.util.Iterator; + +import org.apache.jackrabbit.mk.json.JsopReader; import org.apache.jackrabbit.mk.json.JsopTokenizer; -import org.apache.jackrabbit.mk.simple.NodeImpl; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeImpl; /** * An index for all values with a given prefix. */ -public class PrefixIndex implements Index { +public class PrefixIndex implements PIndex, PropertyIndexConstants { - private final Indexer indexer; + private final BTreeHelper indexer; private final BTree tree; private final String prefix; - public PrefixIndex(Indexer indexer, String prefix) { + public PrefixIndex(BTreeHelper indexer, String prefix) { this.indexer = indexer; this.prefix = prefix; - this.tree = new BTree(indexer, "prefix:" + prefix, false); + this.tree = new BTree(indexer, TYPE_PREFIX + prefix, false); tree.setMinSize(10); } - public static PrefixIndex fromNodeName(Indexer indexer, String nodeName) { - if (!nodeName.startsWith("prefix:")) { + public static PrefixIndex fromNodeName(BTreeHelper indexer, String nodeName) { + if (!nodeName.startsWith(TYPE_PREFIX)) { return null; } - String prefix = nodeName.substring("prefix:".length()); + String prefix = nodeName.substring(TYPE_PREFIX.length()); return new PrefixIndex(indexer, prefix); } - public String getName() { - return tree.getName(); + public String getPrefix() { + return prefix; } + @Override public void addOrRemoveNode(NodeImpl node, boolean add) { String nodePath = node.getPath(); for (int i = 0, size = node.getPropertyCount(); i < size; i++) { @@ -57,10 +61,11 @@ public void addOrRemoveNode(NodeImpl node, boolean add) { } } + @Override public void addOrRemoveProperty(String nodePath, String propertyName, String value, boolean add) { JsopTokenizer t = new JsopTokenizer(value); - if (t.matches(JsopTokenizer.STRING)) { + if (t.matches(JsopReader.STRING)) { String v = t.getToken(); if (v.startsWith(prefix)) { addOrRemove(nodePath, propertyName, v, add); @@ -68,18 +73,18 @@ public void addOrRemoveProperty(String nodePath, String propertyName, } else if (t.matches('[')) { if (!t.matches(']')) { do { - if (t.matches(JsopTokenizer.STRING)) { + if (t.matches(JsopReader.STRING)) { String v = t.getToken(); if (v.startsWith(prefix)) { addOrRemove(nodePath, propertyName, v, add); } - } else if (t.matches(JsopTokenizer.FALSE)) { + } else if (t.matches(JsopReader.FALSE)) { // ignore - } else if (t.matches(JsopTokenizer.TRUE)) { + } else if (t.matches(JsopReader.TRUE)) { // ignore - } else if (t.matches(JsopTokenizer.NULL)) { + } else if (t.matches(JsopReader.NULL)) { // ignore - } else if (t.matches(JsopTokenizer.NUMBER)) { + } else if (t.matches(JsopReader.NUMBER)) { // ignore } } while (t.matches(',')); @@ -90,10 +95,11 @@ public void addOrRemoveProperty(String nodePath, String propertyName, private void addOrRemove(String path, String propertyName, String value, boolean add) { String v = value.substring(prefix.length()); + String p = PathUtils.concat(path, propertyName); if (add) { - tree.add(v, path + "/" + propertyName); + tree.add(v, p); } else { - tree.remove(v, path + "/" + propertyName); + tree.remove(v, p); } } @@ -105,6 +111,7 @@ private void addOrRemove(String path, String propertyName, String value, boolean * @return an iterator of the paths (an empty iterator if not found) * @throws IllegalArgumentException if the value doesn't start with the prefix */ + @Override public Iterator getPaths(String value, String revision) { if (!value.startsWith(prefix)) { throw new IllegalArgumentException( @@ -116,4 +123,14 @@ public Iterator getPaths(String value, String revision) { return new Cursor.RangeIterator(c, v); } + @Override + public String getIndexNodeName() { + return tree.getName(); + } + + @Override + public boolean isUnique() { + return tree.isUnique(); + } + } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PropertyContentIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PropertyContentIndex.java new file mode 100644 index 00000000000..2992377642a --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PropertyContentIndex.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.old; + +import java.util.Iterator; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.query.index.IndexRowImpl; +import org.apache.jackrabbit.oak.spi.query.Cursor; +import org.apache.jackrabbit.oak.spi.query.Filter; +import org.apache.jackrabbit.oak.spi.query.IndexRow; +import org.apache.jackrabbit.oak.spi.query.QueryIndex; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * An index that stores the index data in a {@code MicroKernel}. + * + * @deprecated the revisionId info has been removed + */ +public class PropertyContentIndex implements QueryIndex { + + private final PropertyIndex index; + + public PropertyContentIndex(PropertyIndex index) { + this.index = index; + } + + @Override + public double getCost(Filter filter, NodeState root) { + String propertyName = index.getPropertyName(); + Filter.PropertyRestriction restriction = filter.getPropertyRestriction(propertyName); + if (restriction == null) { + return Double.MAX_VALUE; + } + if (restriction.first != restriction.last) { + // only support equality matches (for now) + return Double.MAX_VALUE; + } + boolean unique = index.isUnique(); + return unique ? 2 : 20; + } + + @Override + public String getPlan(Filter filter, NodeState root) { + String propertyName = index.getPropertyName(); + Filter.PropertyRestriction restriction = filter.getPropertyRestriction(propertyName); + return "propertyIndex \"" + restriction.propertyName + " " + restriction.toString() + '"'; + } + + @Override + public Cursor query(Filter filter, NodeState root) { + String propertyName = index.getPropertyName(); + Filter.PropertyRestriction restriction = filter.getPropertyRestriction(propertyName); + if (restriction == null) { + throw new IllegalArgumentException("No restriction for " + propertyName); + } + PropertyValue first = restriction.first; + String f = first == null ? null : first.getValue(Type.STRING); + // TODO revisit code after the removal of revisionId + String revisionId = ""; + Iterator it = index.getPaths(f, revisionId); + return new ContentCursor(it); + } + + + @Override + public String getIndexName() { + return index.getIndexNodeName(); + } + + /** + * The cursor to for this index. + */ + static class ContentCursor implements Cursor { + + private final Iterator it; + + private String currentPath; + + public ContentCursor(Iterator it) { + this.it = it; + } + + @Override + public IndexRow currentRow() { + return new IndexRowImpl(currentPath); + } + + @Override + public boolean next() { + if (it.hasNext()) { + currentPath = it.next(); + return true; + } + return false; + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/index/PropertyIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PropertyIndex.java similarity index 73% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/index/PropertyIndex.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PropertyIndex.java index ad521756123..60a994f852f 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/index/PropertyIndex.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PropertyIndex.java @@ -14,48 +14,51 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.index; +package org.apache.jackrabbit.oak.plugins.index.old; import java.util.Iterator; + +import org.apache.jackrabbit.mk.json.JsopReader; import org.apache.jackrabbit.mk.json.JsopTokenizer; -import org.apache.jackrabbit.mk.simple.NodeImpl; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeImpl; /** * A node handler that maps the property value to the key, and the path of the * node to the value. Only string and numbers are indexes (arrays, true, false, * and null are not indexes). */ -public class PropertyIndex implements Index { +public class PropertyIndex implements PIndex, PropertyIndexConstants { - private final Indexer indexer; + private final BTreeHelper indexer; private final BTree tree; private final String propertyName; - public PropertyIndex(Indexer indexer, String propertyName, boolean unique) { + public PropertyIndex(BTreeHelper indexer, String propertyName, boolean unique) { this.indexer = indexer; this.propertyName = propertyName; - this.tree = new BTree(indexer, (unique ? "id:" : "property:") + propertyName, unique); + this.tree = new BTree(indexer, TYPE_PROPERTY + propertyName + + (unique ? "," + UNIQUE : ""), unique); tree.setMinSize(10); } - public static PropertyIndex fromNodeName(Indexer indexer, String nodeName) { - boolean unique; - if (nodeName.startsWith("property:")) { - unique = false; - } else if (nodeName.startsWith("id:")) { - unique = true; - } else { + public static PropertyIndex fromNodeName(BTreeHelper indexer, String nodeName) { + if (!nodeName.startsWith(TYPE_PROPERTY)) { return null; } - int index = nodeName.indexOf(':'); - String propertyName = nodeName.substring(0, index); - return new PropertyIndex(indexer, propertyName, unique); + boolean unique = false; + if (nodeName.endsWith(UNIQUE)) { + unique = true; + nodeName = nodeName.substring(0, nodeName.length() - UNIQUE.length() - 1); + } + String property = nodeName.substring(TYPE_PROPERTY.length()); + return new PropertyIndex(indexer, property, unique); } - public String getName() { - return tree.getName(); + public String getPropertyName() { + return propertyName; } + @Override public void addOrRemoveNode(NodeImpl node, boolean add) { String value = node.getProperty(propertyName); if (value != null) { @@ -63,6 +66,7 @@ public void addOrRemoveNode(NodeImpl node, boolean add) { } } + @Override public void addOrRemoveProperty(String nodePath, String propertyName, String value, boolean add) { if (this.propertyName.equals(propertyName)) { @@ -72,7 +76,7 @@ public void addOrRemoveProperty(String nodePath, String propertyName, private void addOrRemoveRaw(String nodePath, String value, boolean add) { JsopTokenizer t = new JsopTokenizer(value); - if (t.matches(JsopTokenizer.STRING) || t.matches(JsopTokenizer.NUMBER)) { + if (t.matches(JsopReader.STRING) || t.matches(JsopReader.NUMBER)) { String v = t.getToken(); addOrRemove(nodePath, v, add); } @@ -116,10 +120,21 @@ public String getPath(String propertyValue, String revision) { * @param revision the revision * @return an iterator of the paths (an empty iterator if not found) */ + @Override public Iterator getPaths(String propertyValue, String revision) { indexer.updateUntil(revision); Cursor c = tree.findFirst(propertyValue); return new Cursor.RangeIterator(c, propertyValue); } + @Override + public String getIndexNodeName() { + return tree.getName(); + } + + @Override + public boolean isUnique() { + return tree.isUnique(); + } + } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PropertyIndexConstants.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PropertyIndexConstants.java new file mode 100644 index 00000000000..79b2a3ca39a --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PropertyIndexConstants.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.plugins.index.old; + +import org.apache.jackrabbit.oak.plugins.index.IndexUtils; + +public interface PropertyIndexConstants { + + String INDEX_TYPE_PROPERTY = "property"; + + /** + * The root node of the index definition (configuration) nodes. + */ + // TODO OAK-178 discuss where to store index config data + String INDEX_CONFIG_PATH = "/" + IndexUtils.INDEX_DEFINITIONS_NAME + "/indexes"; + // "/jcr:system/indexes"; + + /** + * For each index, the index content is stored relative to the index + * definition below this node. There is also such a node just below the + * index definition node, to store the last revision and for temporary data. + */ + String INDEX_CONTENT = ":data"; + + /** + * The node name prefix of a prefix index. + */ + String TYPE_PREFIX = "prefix@"; + + /** + * The node name prefix of a property index. + */ + // TODO support multi-property indexes + String TYPE_PROPERTY = "property@"; + + /** + * Marks a unique index. + */ + String UNIQUE = "unique"; + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PropertyIndexer.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PropertyIndexer.java new file mode 100644 index 00000000000..ea9c2303834 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/PropertyIndexer.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.old; + +import static org.apache.jackrabbit.oak.commons.PathUtils.elements; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.plugins.index.IndexDefinition; +import org.apache.jackrabbit.oak.plugins.index.IndexUtils; +import org.apache.jackrabbit.oak.spi.commit.CommitHook; +import org.apache.jackrabbit.oak.spi.query.QueryIndex; +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +public class PropertyIndexer implements QueryIndexProvider, CommitHook, + PropertyIndexConstants { + + private final String indexConfigPath = "/"; + + private final Indexer indexer; + + public PropertyIndexer(Indexer indexer) { + this.indexer = indexer; + } + + @Override + public NodeState processCommit(NodeState before, NodeState after) + throws CommitFailedException { + // TODO update index data see OAK-298 + return after; + } + + @Override + @Nonnull + public List getQueryIndexes(NodeState nodeState) { + List queryIndexList = new ArrayList(); + List indexDefinitions = IndexUtils + .buildIndexDefinitions(nodeState, indexConfigPath, + INDEX_TYPE_PROPERTY); + for (IndexDefinition def : indexDefinitions) { + NodeBuilder builder = childBuilder(nodeState, def.getPath()); + for (String k : builder.getChildNodeNames()) { + PropertyIndex prop = PropertyIndex.fromNodeName(indexer, k); + if (prop != null) { + // create the :data node + builder.child(prop.getIndexNodeName()).child(INDEX_CONTENT); + queryIndexList.add(new PropertyContentIndex(prop)); + } + PrefixIndex pref = PrefixIndex.fromNodeName(indexer, k); + if (pref != null) { + // create the :data node + builder.child(pref.getIndexNodeName()).child(INDEX_CONTENT); + queryIndexList.add(new PrefixContentIndex(pref)); + } + } + // create the global :data node + builder.child(INDEX_CONTENT); + } + return queryIndexList; + } + + private static NodeBuilder childBuilder(NodeState state, String path) { + NodeBuilder builder = state.builder(); + for (String p : elements(path)) { + builder = builder.child(p); + } + return builder; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/ExceptionFactory.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/ExceptionFactory.java similarity index 82% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/util/ExceptionFactory.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/ExceptionFactory.java index 85056c632af..6afccb2de4b 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/ExceptionFactory.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/ExceptionFactory.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.util; +package org.apache.jackrabbit.oak.plugins.index.old.mk; import java.io.FileInputStream; import java.io.IOException; @@ -27,7 +27,7 @@ */ public class ExceptionFactory { - private static final String POM = "META-INF/maven/org.apache.jackrabbit/microkernel/pom.properties"; + private static final String POM = "META-INF/maven/org.apache.jackrabbit/oak-core/pom.properties"; private static String version; @@ -49,14 +49,10 @@ public static String getVersion() { if (in == null) { in = new FileInputStream("target/maven-archiver/pom.properties"); } - if (in == null) { - version = ""; - } else { - Properties prop = new Properties(); - prop.load(in); - in.close(); - version = "[" + prop.getProperty("artifactId") + "-" + prop.getProperty("version") + "]"; - } + Properties prop = new Properties(); + prop.load(in); + in.close(); + version = "[" + prop.getProperty("artifactId") + "-" + prop.getProperty("version") + "]"; } catch (IOException e) { version = ""; } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/IndexWrapper.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/IndexWrapper.java new file mode 100644 index 00000000000..994568ddc95 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/IndexWrapper.java @@ -0,0 +1,210 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.old.mk; + +import java.io.InputStream; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.api.MicroKernelException; +import org.apache.jackrabbit.mk.json.JsopReader; +import org.apache.jackrabbit.mk.json.JsopStream; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.plugins.index.old.Indexer; +import org.apache.jackrabbit.oak.plugins.index.old.PrefixIndex; +import org.apache.jackrabbit.oak.plugins.index.old.PropertyIndex; +import org.apache.jackrabbit.oak.plugins.index.old.PropertyIndexConstants; +import org.apache.jackrabbit.oak.plugins.index.old.mk.wrapper.MicroKernelWrapper; +import org.apache.jackrabbit.oak.plugins.index.old.mk.wrapper.MicroKernelWrapperBase; + +/** + * The index mechanism, as a wrapper. + * + * @deprecated - see OAK-298 + */ +public class IndexWrapper extends MicroKernelWrapperBase implements MicroKernel { + + private final MicroKernelWrapper mk; + private final Indexer indexer; + private final Set branchRevisions = Collections.synchronizedSet(new HashSet()); + + public IndexWrapper(MicroKernel mk) { + this.mk = MicroKernelWrapperBase.wrap(mk); + this.indexer = new Indexer(mk); + indexer.init(); + } + + public IndexWrapper(MicroKernel mk, String indexConfigPath) { + this.mk = MicroKernelWrapperBase.wrap(mk); + this.indexer = new Indexer(mk, indexConfigPath); + indexer.init(); + } + + public Indexer getIndexer() { + return indexer; + } + + @Override + public String getHeadRevision() { + return mk.getHeadRevision(); + } + + @Override + public long getLength(String blobId) { + return mk.getLength(blobId); + } + + @Override + public boolean nodeExists(String path, String revisionId) { + String indexRoot = indexer.getIndexRootNode(); + if (path.startsWith(indexRoot)) { + return false; + } + return mk.nodeExists(path, revisionId); + } + + @Override + public long getChildNodeCount(String path, String revisionId) { + return mk.getChildNodeCount(path, revisionId); + } + + @Override + public int read(String blobId, long pos, byte[] buff, int off, int length) { + return mk.read(blobId, pos, buff, off, length); + } + + @Override + public String waitForCommit(String oldHeadRevisionId, long maxWaitMillis) throws MicroKernelException, InterruptedException { + return mk.waitForCommit(oldHeadRevisionId, maxWaitMillis); + } + + @Override + public String write(InputStream in) { + return mk.write(in); + } + + @Override + public String branch(String trunkRevisionId) { + String branchRevision = mk.branch(trunkRevisionId); + branchRevisions.add(branchRevision); + return branchRevision; + } + + @Override + public String merge(String branchRevisionId, String message) { + String headRevision = mk.merge(branchRevisionId, message); + branchRevisions.remove(branchRevisionId); + indexer.updateUntil(headRevision); + return mk.getHeadRevision(); + } + + @Override + public String commitStream(String rootPath, JsopReader jsonDiff, String revisionId, String message) { + if (branchRevisions.remove(revisionId)) { + // TODO update the index in the branch as well, if required + String rev = mk.commitStream(rootPath, jsonDiff, revisionId, message); + branchRevisions.add(rev); + return rev; + } + String rev = mk.commitStream(rootPath, jsonDiff, revisionId, message); + jsonDiff.resetReader(); + indexer.updateIndex(rootPath, jsonDiff, rev); + rev = mk.getHeadRevision(); + rev = indexer.updateEnd(rev); + return rev; + } + + @Override + public JsopReader getNodesStream(String path, String revisionId, int depth, long offset, int count, String filter) { + String indexRoot = indexer.getIndexRootNode(); + if (!path.startsWith(indexRoot)) { + return mk.getNodesStream(path, revisionId, depth, offset, count, filter); + } + String index = PathUtils.relativize(indexRoot, path); + int idx = index.indexOf('?'); + if (idx < 0) { + // not a query (expected: /index/prefix:x?y) - treat as regular node lookup + return mk.getNodesStream(path, revisionId, depth, offset, count, filter); + } + String data = index.substring(idx + 1); + index = index.substring(0, idx); + JsopStream s = new JsopStream(); + s.array(); + if (index.startsWith(PropertyIndexConstants.TYPE_PREFIX)) { + String prefix = index.substring(PropertyIndexConstants.TYPE_PREFIX.length()); + PrefixIndex prefixIndex = indexer.getPrefixIndex(prefix); + if (prefixIndex == null) { + throw ExceptionFactory.get("Unknown index: " + index); + } + Iterator it = prefixIndex.getPaths(data, revisionId); + while (it.hasNext()) { + s.value(it.next()); + } + } else if (index.startsWith(PropertyIndexConstants.TYPE_PROPERTY)) { + String property = index.substring(PropertyIndexConstants.TYPE_PROPERTY.length()); + boolean unique = false; + if (property.endsWith("," + PropertyIndexConstants.UNIQUE)) { + unique = true; + property = property.substring(0, property.length() - PropertyIndexConstants.UNIQUE.length() - 1); + } + PropertyIndex propertyIndex = indexer.getPropertyIndex(property); + if (propertyIndex == null) { + throw ExceptionFactory.get("Unknown index: " + index); + } + if (unique) { + String value = propertyIndex.getPath(data, revisionId); + if (value != null) { + s.value(value); + } + } else { + Iterator it = propertyIndex.getPaths(data, revisionId); + while (it.hasNext()) { + s.value(it.next()); + } + } + } + s.endArray(); + return s; + } + + @Override + public JsopReader diffStream(String fromRevisionId, String toRevisionId, String path, int depth) { + return mk.diffStream(fromRevisionId, toRevisionId, path, depth); + } + + @Override + public JsopReader getJournalStream(String fromRevisionId, String toRevisionId, String path) { + return mk.getJournalStream(fromRevisionId, toRevisionId, path); + } + + @Override + public JsopReader getRevisionsStream(long since, int maxEntries, String path) { + return mk.getRevisionsStream(since, maxEntries, path); + } + + public void dispose() { + // do nothing + } + + public MicroKernel getBaseKernel() { + return mk; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/MicroKernelFactory.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/MicroKernelFactory.java new file mode 100644 index 00000000000..3f3951cd3af --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/MicroKernelFactory.java @@ -0,0 +1,225 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.old.mk; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.api.MicroKernelException; +import org.apache.jackrabbit.mk.client.Client; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +import org.apache.jackrabbit.mk.server.Server; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.SimpleKernelImpl; +import org.apache.jackrabbit.oak.plugins.index.old.mk.wrapper.LogWrapper; +import org.apache.jackrabbit.oak.plugins.index.old.mk.wrapper.SecurityWrapper; +import org.apache.jackrabbit.oak.plugins.index.old.mk.wrapper.VirtualRepositoryWrapper; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * A factory to create a MicroKernel instance. + */ +public class MicroKernelFactory { + + private static final Map INSTANCES = + new HashMap(); + + private MicroKernelFactory() { + } + + /** + * Get an instance. Supported URLs: + *
    + *
  • fs:target/mk-test (using the directory ./target/mk-test)
  • + *
  • fs:target/mk-test;clean (same, but delete the old repository first)
  • + *
  • fs:{homeDir} (use the system property homeDir or '.' if not set)
  • + *
  • simple: (in-memory implementation)
  • + *
  • simple:fs:target/temp (using the directory ./target/temp)
  • + *
+ * + * @param url the repository URL + * @return a new instance + */ + public static MicroKernel getInstance(String url) { + int colon = url.indexOf(':'); + if (colon == -1) { + throw new IllegalArgumentException("Unknown repository URL: " + url); + } + + String head = url.substring(0, colon); + String tail = url.substring(colon + 1); + if (head.equals("mem") || head.equals("simple") || head.equals("fs")) { + boolean clean = false; + if (tail.endsWith(";clean")) { + tail = tail.substring(0, tail.length() - ";clean".length()); + clean = true; + } + + tail = tail.replaceAll("\\{homeDir\\}", System.getProperty("homeDir", ".")); + + if (clean) { + // TODO: The factory should not control repository lifecycle + // See also https://issues.apache.org/jira/browse/OAK-32 + String dir; + if (head.equals("fs")) { + dir = tail + "/.mk"; + } else { + dir = tail.substring(tail.lastIndexOf(':') + 1); + INSTANCES.remove(tail); + } + deleteRecursive(new File(dir)); + } + + if (head.equals("fs")) { + return new MicroKernelImpl(tail); + } else { + final String name = tail; + synchronized (INSTANCES) { + SimpleKernelImpl instance = INSTANCES.get(name); + if (instance == null) { + instance = new SimpleKernelImpl(name) { + @Override + public synchronized void dispose() { + super.dispose(); + synchronized (INSTANCES) { + INSTANCES.remove(name); + } + } + }; + INSTANCES.put(name, instance); + } + return instance; + } + } + } else if (head.equals("log")) { + return new LogWrapper(getInstance(tail)); + } else if (head.equals("sec")) { + String userPassUrl = tail; + int index = userPassUrl.indexOf(':'); + if (index < 0) { + throw ExceptionFactory.get("Expected url format: sec:user@pass:"); + } + String u = userPassUrl.substring(index + 1); + String userPass = userPassUrl.substring(0, index); + index = userPass.indexOf('@'); + if (index < 0) { + throw ExceptionFactory.get("Expected url format: sec:user@pass:"); + } + String user = userPass.substring(0, index); + String pass = userPass.substring(index + 1); + final MicroKernel mk = getInstance(u); + try { + return new SecurityWrapper(mk, user, pass) { + @Override + public void dispose() { + super.dispose(); + MicroKernelFactory.disposeInstance(mk); + } + }; + } catch (MicroKernelException e) { + MicroKernelFactory.disposeInstance(mk); + throw e; + } + } else if (head.equals("virtual")) { + final MicroKernel mk = getInstance(tail); + try { + return new VirtualRepositoryWrapper(mk) { + @Override + public void dispose() { + super.dispose(); + MicroKernelFactory.disposeInstance(mk); + } + }; + } catch (MicroKernelException e) { + MicroKernelFactory.disposeInstance(mk); + throw e; + } + } else if (head.equals("index")) { + final MicroKernel mk = getInstance(tail); + try { + return new IndexWrapper(mk) { + public void dispose() { + MicroKernelFactory.disposeInstance(mk); + } + }; + } catch (MicroKernelException e) { + MicroKernelFactory.disposeInstance(mk); + throw e; + } + } else if (head.equals("http")) { + return new Client(url); + } else if (head.equals("http-bridge")) { + final MicroKernel mk = getInstance(tail); + + final Server server = new Server(mk); + try { + server.start(); + } catch (IOException e) { + throw new IllegalArgumentException(e.getMessage()); + } + + return new Client(server.getAddress()) { + @Override + public synchronized void dispose() { + super.dispose(); + server.stop(); + MicroKernelFactory.disposeInstance(mk); + } + }; + } else { + throw new IllegalArgumentException(url); + } + } + + /** + * Disposes an instance that was created by this factory. + * @param mk MicroKernel instance + */ + public static void disposeInstance(MicroKernel mk) { + if (mk instanceof MicroKernelImpl) { + ((MicroKernelImpl) mk).dispose(); + } else if (mk instanceof SimpleKernelImpl) { + ((SimpleKernelImpl) mk).dispose(); + } else if (mk instanceof Client) { + ((Client) mk).dispose(); + } else if (mk instanceof LogWrapper) { + ((LogWrapper) mk).dispose(); + } else if (mk instanceof SecurityWrapper) { + ((SecurityWrapper) mk).dispose(); + } else if (mk instanceof VirtualRepositoryWrapper) { + ((VirtualRepositoryWrapper) mk).dispose(); + } else if (mk instanceof IndexWrapper) { + ((IndexWrapper) mk).dispose(); + } else { + throw new IllegalArgumentException("instance was not created by this factory"); + } + } + + /** + * Delete a directory or file and all subdirectories and files inside it. + * + * @param file the file denoting the directory to delete + */ + private static void deleteRecursive(File file) { + File[] files = file.listFiles(); + for (int i = 0; files != null && i < files.length; i++) { + deleteRecursive(files[i]); + } + file.delete(); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/AscendingClock.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/AscendingClock.java similarity index 95% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/util/AscendingClock.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/AscendingClock.java index cbbb72cd706..1864f2047c6 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/AscendingClock.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/AscendingClock.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.util; +package org.apache.jackrabbit.oak.plugins.index.old.mk.simple; /** * A clock that normally returns the current system time since 1970, and is @@ -23,7 +23,7 @@ * returns incrementing values. Unique nanosecond values are returned for system * times between the years 1970 and 2554. */ -public class AscendingClock { +class AscendingClock { /** * The offset between System.nanoTime() (which returns elapsed time) and the @@ -45,7 +45,7 @@ public class AscendingClock { /** * Create a new clock. * - * @param last the time (the next returned value will be at least one + * @param lastMillis the time (the next returned value will be at least one * bigger) */ public AscendingClock(long lastMillis) { diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeId.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeId.java similarity index 94% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeId.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeId.java index 237ba1ef426..166ea9bbef4 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeId.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeId.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.simple; +package org.apache.jackrabbit.oak.plugins.index.old.mk.simple; /** * A node id. @@ -42,6 +42,7 @@ public NodeImpl getNode(NodeMap map) { return map.getNode(x); } + @Override public String toString() { return Long.toString(x); } @@ -74,14 +75,17 @@ protected NodeIdInline(NodeImpl node) { this.node = node; } + @Override public NodeImpl getNode(NodeMap map) { return node; } + @Override public String toString() { return node.toString(); } + @Override public boolean isInline() { return true; } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeImpl.java similarity index 94% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeImpl.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeImpl.java index 27b759e5e0e..001d339085c 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeImpl.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeImpl.java @@ -14,25 +14,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.simple; +package org.apache.jackrabbit.oak.plugins.index.old.mk.simple; -import java.io.OutputStream; -import java.security.DigestOutputStream; -import java.security.MessageDigest; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import org.apache.jackrabbit.mk.Constants; import org.apache.jackrabbit.mk.json.JsopBuilder; import org.apache.jackrabbit.mk.json.JsopReader; import org.apache.jackrabbit.mk.json.JsopTokenizer; import org.apache.jackrabbit.mk.json.JsopWriter; import org.apache.jackrabbit.mk.util.Cache; -import org.apache.jackrabbit.mk.util.ExceptionFactory; import org.apache.jackrabbit.mk.util.IOUtils; -import org.apache.jackrabbit.mk.util.PathUtils; -import org.apache.jackrabbit.mk.util.StringCache; import org.apache.jackrabbit.mk.util.StringUtils; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.plugins.index.old.mk.ExceptionFactory; + +import java.io.OutputStream; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; /** * An in-memory node, including all child nodes. @@ -42,7 +41,7 @@ public class NodeImpl implements Cache.Value { /** * The child node count. */ - public static final String CHILREN_COUNT = ":childNodeCount"; + public static final String CHILDREN_COUNT = ":childNodeCount"; /** * The total number of child nodes. @@ -81,6 +80,8 @@ public class NodeImpl implements Cache.Value { */ static final String COUNT = ":childCount"; + private static final boolean NODE_NAME_AS_PROPERTY = false; + /** * The node name. */ @@ -177,7 +178,8 @@ public NodeImpl getNode(String path) { } private NodeImpl getChildNode(String name) { - return childNodes.get(name).getNode(map); + NodeId id = childNodes.get(name); + return id == null ? null : id.getNode(map); } public NodeImpl cloneAndAddChildNode(String path, boolean before, String position, NodeImpl newNode, long revId) { @@ -196,11 +198,11 @@ public NodeImpl cloneAndAddChildNode(String path, boolean before, String positio throw ExceptionFactory.get("Node not found: " + path); } long diffDescendant = -n.descendantCount; - long diffInline = -n.descendantInlineCount; + // long diffInline = -n.descendantInlineCount; NodeImpl n2 = n.cloneAndAddChildNode(path.substring(index + 1), before, position, newNode, revId); NodeImpl clone = setChild(child, n2, revId); diffDescendant += n2.descendantCount; - diffInline += n2.descendantInlineCount; + // diffInline += n2.descendantInlineCount; clone.descendantCount += diffDescendant; clone.descendantInlineCount += diffDescendant; return clone; @@ -305,11 +307,11 @@ public void append(JsopWriter json, int depth, long offset, int count, boolean c } if (childNodes == null) { if (childNodeCount) { - json.key(CHILREN_COUNT).value(0); + json.key(CHILDREN_COUNT).value(0); } } else { if (childNodeCount) { - json.key(CHILREN_COUNT).value(childNodes.size()); + json.key(CHILDREN_COUNT).value(childNodes.size()); } if (descendantCount > childNodes.size()) { if (map.getDescendantCount()) { @@ -342,7 +344,7 @@ void addChildNode(String name, boolean before, String position, NodeImpl node) { } else if (childNodes.containsKey(name)) { throw ExceptionFactory.get("Node already exists: " + name); } - if (Constants.NODE_NAME_AS_PROPERTY) { + if (NODE_NAME_AS_PROPERTY) { node.setProperty(NAME, JsopBuilder.encode(name)); } childNodes.add(name, map.addNode(node)); @@ -467,7 +469,7 @@ public static NodeImpl parse(NodeMap map, JsopReader t, long revId, String path) } else { String value = t.readRawValue().trim(); if (key.length() > 0 && key.charAt(0) == ':') { - if (key.equals(CHILREN_COUNT)) { + if (key.equals(CHILDREN_COUNT)) { node.totalChildNodeCount = Long.parseLong(value); } else if (key.equals(HASH)) { value = JsopTokenizer.decodeQuoted(value); @@ -524,6 +526,7 @@ public String getPropertyValue(int index) { return propertyValuePairs[index + index + 1]; } + @Override public String toString() { String s = asString(); if (path != null) { @@ -537,12 +540,15 @@ public byte[] getHash() { try { MessageDigest d = MessageDigest.getInstance("SHA-1"); DigestOutputStream out = new DigestOutputStream(new OutputStream() { + @Override public void write(byte[] buff, int off, int length) { // ignore } + @Override public void write(byte[] buff) { // ignore } + @Override public void write(int b) { // ignore } @@ -619,17 +625,14 @@ public static NodeImpl fromString(NodeMap map, String s) { String key = t.readString(); t.read(':'); String value = t.readRawValue(); - if (key.length() > 0 && key.charAt(0) == ':') { - if (key.equals(CHILDREN)) { - node.childNodes = NodeListTrie.read(t, map, value); - } else if (key.equals(DESCENDANT_COUNT)) { - node.descendantCount = Long.parseLong(value); - descendantCountSet = true; - } else if (key.equals(HASH)) { - node.id.setHash(StringUtils.convertHexToBytes(JsopTokenizer.decodeQuoted(value))); - } else { - node.setProperty(key, value); - } + boolean hidden = key.length() > 0 && key.charAt(0) == ':'; + if (hidden && key.equals(CHILDREN)) { + node.childNodes = NodeListTrie.read(t, map, value); + } else if (hidden && key.equals(DESCENDANT_COUNT)) { + node.descendantCount = Long.parseLong(value); + descendantCountSet = true; + } else if (hidden && key.equals(HASH)) { + node.id.setHash(StringUtils.convertHexToBytes(JsopTokenizer.decodeQuoted(value))); } else if (map.isId(value)) { if (node.childNodes == null) { node.childNodes = new NodeListSmall(); @@ -685,10 +688,14 @@ void visit(ChildVisitor v) { } } + /** + * A visitor over the child nodes. + */ interface ChildVisitor { void accept(NodeId childId); } + @Override public int getMemory() { if (memory == 0) { String[] pv = propertyValuePairs; @@ -704,6 +711,7 @@ public int getMemory() { return memory; } + @Override public int hashCode() { int hash = Arrays.hashCode(propertyValuePairs); if (childNodes != null) { @@ -712,6 +720,7 @@ public int hashCode() { return hash; } + @Override public boolean equals(Object other) { if (other == this) { return true; diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeList.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeList.java similarity index 94% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeList.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeList.java index 64a1cd44d39..619220061ec 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeList.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeList.java @@ -14,13 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.simple; +package org.apache.jackrabbit.oak.plugins.index.old.mk.simple; import java.io.IOException; import java.io.OutputStream; import java.util.Iterator; import org.apache.jackrabbit.mk.json.JsopWriter; -import org.apache.jackrabbit.mk.simple.NodeImpl.ChildVisitor; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeImpl.ChildVisitor; /** * A list of child nodes. @@ -129,6 +129,7 @@ interface NodeList { * * @param map the node map * @param out the output stream + * @throws IOException if writing to the stream failed */ void updateHash(NodeMap map, OutputStream out) throws IOException; diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeListSmall.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeListSmall.java similarity index 92% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeListSmall.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeListSmall.java index 92f2dba033c..90d464dd536 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeListSmall.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeListSmall.java @@ -14,20 +14,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.simple; +package org.apache.jackrabbit.oak.plugins.index.old.mk.simple; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.Iterator; + import org.apache.jackrabbit.mk.json.JsopBuilder; import org.apache.jackrabbit.mk.json.JsopWriter; -import org.apache.jackrabbit.mk.simple.NodeImpl.ChildVisitor; -import org.apache.jackrabbit.mk.util.ArrayUtils; -import org.apache.jackrabbit.mk.util.ExceptionFactory; +import org.apache.jackrabbit.oak.plugins.index.old.mk.ExceptionFactory; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeImpl.ChildVisitor; +import org.apache.jackrabbit.oak.util.ArrayUtils; import org.apache.jackrabbit.mk.util.IOUtils; -import org.apache.jackrabbit.mk.util.StringCache; /** * A list of child nodes that fits in memory. @@ -70,10 +70,12 @@ private NodeListSmall(String[] names, NodeId[] children, int[] sort, int size) { this.size = size; } + @Override public long size() { return size; } + @Override public boolean containsKey(String name) { return find(name) >= 0; } @@ -102,14 +104,16 @@ private int find(String name) { return -(min + 1); } + @Override public NodeId get(String name) { int index = find(name); if (index < 0) { - throw ExceptionFactory.get("Node not found: " + name); + return null; } return children[sort[index]]; } + @Override public void add(String name, NodeId x) { int index = find(name); if (index >= 0) { @@ -123,6 +127,7 @@ public void add(String name, NodeId x) { size++; } + @Override public void replace(String name, NodeId x) { int index = find(name); if (index < 0) { @@ -131,27 +136,33 @@ public void replace(String name, NodeId x) { children = ArrayUtils.arrayReplace(children, sort[index], x); } + @Override public String getName(long pos) { return pos >= names.length ? null : names[(int) pos]; } + @Override public Iterator getNames(final long offset, final int maxCount) { return new Iterator() { int pos = (int) offset; int remaining = maxCount; + @Override public boolean hasNext() { return pos < size && remaining > 0; } + @Override public String next() { remaining--; return names[pos++]; } + @Override public void remove() { throw new UnsupportedOperationException(); } }; } + @Override public NodeId remove(String name) { int index = find(name); if (index < 0) { @@ -173,6 +184,7 @@ public NodeId remove(String name) { return result; } + @Override public String toString() { JsopWriter json = new JsopBuilder(); json.object(); @@ -183,6 +195,7 @@ public String toString() { return json.toString(); } + @Override public NodeList createClone(NodeMap map, long revId) { NodeList result = new NodeListSmall(names, children, sort, size); if (size > map.getMaxMemoryChildren()) { @@ -191,12 +204,14 @@ public NodeList createClone(NodeMap map, long revId) { return result; } + @Override public void visit(ChildVisitor v) { for (NodeId c : children) { v.accept(c); } } + @Override public void append(JsopWriter json, NodeMap map) { for (int i = 0; i < size; i++) { json.key(names[i]); @@ -209,6 +224,7 @@ public void append(JsopWriter json, NodeMap map) { } } + @Override public int getMemory() { int memory = 100; for (int i = 0; i < names.length; i++) { @@ -217,6 +233,7 @@ public int getMemory() { return memory; } + @Override public int hashCode() { if (size == 0) { return 0; @@ -226,6 +243,7 @@ public int hashCode() { Arrays.hashCode(sort); } + @Override public boolean equals(Object other) { if (other == this) { return true; @@ -244,6 +262,7 @@ public boolean equals(Object other) { return false; } + @Override public void updateHash(NodeMap map, OutputStream out) throws IOException { if (children != null) { try { diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeListTrie.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeListTrie.java similarity index 95% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeListTrie.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeListTrie.java index b4c8e0ace72..e46043eeda2 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeListTrie.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeListTrie.java @@ -14,17 +14,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.simple; +package org.apache.jackrabbit.oak.plugins.index.old.mk.simple; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Iterator; + import org.apache.jackrabbit.mk.json.JsopTokenizer; import org.apache.jackrabbit.mk.json.JsopWriter; -import org.apache.jackrabbit.mk.simple.NodeImpl.ChildVisitor; -import org.apache.jackrabbit.mk.util.ExceptionFactory; import org.apache.jackrabbit.mk.util.IOUtils; +import org.apache.jackrabbit.oak.plugins.index.old.mk.ExceptionFactory; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeImpl.ChildVisitor; /** * A large list of nodes, using a data structure similar to a trie. @@ -80,7 +81,7 @@ private NodeListTrie(NodeMap map, ArrayList children, int prefixLength, l this.size = size; } - private String getPrefix(String name, int len) { + private static String getPrefix(String name, int len) { if (name.length() < len) { return name + new String(new char[len - name.length()]); } @@ -99,6 +100,7 @@ private void addChild(int index, String name, NodeListSmall partList) { children.add(index, c); } + @Override public boolean containsKey(String name) { int index = getChildIndex(name); if (index < 0) { @@ -120,6 +122,7 @@ NodeList getListClone(Child c) { return n.getNodeList(); } + @Override public NodeId get(String name) { int index = getChildIndex(name); if (index < 0) { @@ -130,6 +133,7 @@ public NodeId get(String name) { return list.get(name); } + @Override public String getName(long pos) { int i = 0; for (; i < children.size(); i++) { @@ -144,6 +148,7 @@ public String getName(long pos) { return null; } + @Override public Iterator getNames(long offset, final int maxCount) { int i = 0; for (; i < children.size(); i++) { @@ -161,6 +166,7 @@ public Iterator getNames(long offset, final int maxCount) { int remaining = maxCount; long offset = off; Iterator it; + @Override public boolean hasNext() { if (it != null && it.hasNext()) { return true; @@ -175,6 +181,7 @@ public boolean hasNext() { return false; } + @Override public String next() { if (hasNext()) { remaining--; @@ -184,6 +191,7 @@ public String next() { } } + @Override public void remove() { throw new UnsupportedOperationException(); } @@ -210,6 +218,7 @@ private int getChildIndex(String name) { return -(min + 1); } + @Override public void add(String name, NodeId x) { int index = getChildIndex(name); if (index < 0) { @@ -225,6 +234,7 @@ public void add(String name, NodeId x) { size++; } + @Override public void replace(String name, NodeId x) { int index = getChildIndex(name); if (index < 0) { @@ -236,6 +246,7 @@ public void replace(String name, NodeId x) { } } + @Override public NodeId remove(String name) { int index = getChildIndex(name); if (index < 0) { @@ -246,6 +257,7 @@ public NodeId remove(String name) { return list.remove(name); } + @Override public long size() { return size; } @@ -258,12 +270,14 @@ static class Child { NodeId id; String prefix; + @Override public String toString() { return prefix; } } + @Override public NodeList createClone(NodeMap map, long revId) { if (revId == this.revId) { return this; @@ -292,12 +306,14 @@ public NodeList createClone(NodeMap map, long revId) { return result; } + @Override public void visit(ChildVisitor v) { for (Child c : children) { v.accept(c.id); } } + @Override public void append(JsopWriter json, NodeMap map) { for (Child c : children) { json.key(NodeImpl.CHILDREN); @@ -349,10 +365,12 @@ static NodeListTrie read(JsopTokenizer t, NodeMap map, String firstNodeId) { return list; } + @Override public int getMemory() { return children.size() * 100; } + @Override public void updateHash(NodeMap map, OutputStream out) throws IOException { for (Child c : children) { byte[] hash = c.id.getHash(); diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeMap.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeMap.java similarity index 95% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeMap.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeMap.java index b72abc5f843..12b87ec9328 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeMap.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeMap.java @@ -14,15 +14,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.simple; +package org.apache.jackrabbit.oak.plugins.index.old.mk.simple; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; -import org.apache.jackrabbit.mk.simple.NodeImpl.ChildVisitor; -import org.apache.jackrabbit.mk.util.ExceptionFactory; +import org.apache.jackrabbit.oak.plugins.index.old.mk.ExceptionFactory; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeImpl.ChildVisitor; + +/** + * A map of nodes that are accessible by id. + */ public class NodeMap { public static final String MAX_MEMORY_CHILDREN = "maxMemoryChildren"; @@ -109,6 +113,7 @@ public NodeId commit(NodeImpl root) { if (hash) { final NodeMap map = this; root.visit(new ChildVisitor() { + @Override public void accept(NodeId childId) { if (childId.isInline()) { NodeImpl t = childId.getNode(map); diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeMapInDb.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeMapInDb.java similarity index 92% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeMapInDb.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeMapInDb.java index ab197247f88..e2f5231578e 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeMapInDb.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeMapInDb.java @@ -14,8 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.simple; +package org.apache.jackrabbit.oak.plugins.index.old.mk.simple; +import java.io.File; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; @@ -26,11 +27,11 @@ import java.util.HashMap; import java.util.TreeMap; import java.util.Map.Entry; -import org.apache.jackrabbit.mk.fs.FilePath; + import org.apache.jackrabbit.mk.json.JsopBuilder; -import org.apache.jackrabbit.mk.simple.NodeImpl.ChildVisitor; import org.apache.jackrabbit.mk.util.Cache; -import org.apache.jackrabbit.mk.util.ExceptionFactory; +import org.apache.jackrabbit.oak.plugins.index.old.mk.ExceptionFactory; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeImpl.ChildVisitor; /** * A node map that stores data in a database. @@ -49,7 +50,8 @@ public class NodeMapInDb extends NodeMap implements Cache.Backend list = new ArrayList(); newRoot.visit(new ChildVisitor() { + @Override public void accept(NodeId childId) { if (childId.getLong() < 0) { NodeImpl t = temp.get(childId.getLong()); @@ -154,15 +161,18 @@ public void accept(NodeId childId) { return root.getId(); } + @Override public NodeId getId(NodeId id) { long x = id.getLong(); return (x > 0 || !pos.containsKey(x)) ? id : NodeId.get(pos.get(x)); } + @Override public NodeId getRootId() { return root.getId(); } + @Override public NodeImpl getInfo(String path) { NodeImpl n = new NodeImpl(this, 0); n.setProperty("url", JsopBuilder.encode(url)); @@ -177,6 +187,7 @@ public NodeImpl getInfo(String path) { return n; } + @Override public synchronized void close() { try { conn.close(); diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeMapInMongoDb.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeMapInMongoDb.java similarity index 94% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeMapInMongoDb.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeMapInMongoDb.java index e3d1cd2b5d1..305cde4426c 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/NodeMapInMongoDb.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeMapInMongoDb.java @@ -14,15 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.simple; +package org.apache.jackrabbit.oak.plugins.index.old.mk.simple; import java.util.ArrayList; import java.util.HashMap; import java.util.TreeMap; import java.util.Map.Entry; -import org.apache.jackrabbit.mk.simple.NodeImpl.ChildVisitor; + import org.apache.jackrabbit.mk.util.Cache; -import org.apache.jackrabbit.mk.util.ExceptionFactory; +import org.apache.jackrabbit.oak.plugins.index.old.mk.ExceptionFactory; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeImpl.ChildVisitor; + import com.mongodb.BasicDBObject; import com.mongodb.DB; import com.mongodb.DBCollection; @@ -79,6 +81,7 @@ public class NodeMapInMongoDb extends NodeMap implements Cache.Backend list = new ArrayList(); newRoot.visit(new ChildVisitor() { + @Override public void accept(NodeId childId) { if (childId.getLong() < 0) { NodeImpl t = temp.get(childId.getLong()); @@ -161,15 +168,18 @@ public void accept(NodeId childId) { return root.getId(); } + @Override public NodeId getId(NodeId id) { long x = id.getLong(); return (x > 0 || !pos.containsKey(x)) ? id : NodeId.get(pos.get(x)); } + @Override public NodeId getRootId() { return root.getId(); } + @Override public NodeImpl getInfo(String path) { NodeImpl n = new NodeImpl(this, 0); for (Entry e : properties.entrySet()) { @@ -183,6 +193,7 @@ public NodeImpl getInfo(String path) { return n; } + @Override public synchronized void close() { con.close(); } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/Revision.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/Revision.java similarity index 96% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/simple/Revision.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/Revision.java index c8e8a7cf878..5290003db69 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/Revision.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/Revision.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.simple; +package org.apache.jackrabbit.oak.plugins.index.old.mk.simple; import org.apache.jackrabbit.mk.json.JsopBuilder; import org.apache.jackrabbit.mk.json.JsopWriter; @@ -97,14 +97,17 @@ private String getCommitValue(String s) { return ""; } + @Override public int compareTo(Revision o) { return id < o.id ? -1 : id > o.id ? 1 : 0; } + @Override public String toString() { return new JsopBuilder().object(). key("id").value(formatId(id)). key("ts").value(nanos / 1000000). + key("msg").value(getMsg()). endObject().toString(); } @@ -139,6 +142,7 @@ void appendJournal(JsopWriter buff) { endObject().newline(); } + @Override public int getMemory() { return (getDiff().length() + getMsg().length()) * 2; } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/SimpleKernelImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/SimpleKernelImpl.java similarity index 87% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/simple/SimpleKernelImpl.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/SimpleKernelImpl.java index 96e41422bcd..6c04038b75e 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/simple/SimpleKernelImpl.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/SimpleKernelImpl.java @@ -14,30 +14,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.simple; +package org.apache.jackrabbit.oak.plugins.index.old.mk.simple; import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.api.MicroKernelException; import org.apache.jackrabbit.mk.blobs.AbstractBlobStore; import org.apache.jackrabbit.mk.blobs.FileBlobStore; import org.apache.jackrabbit.mk.blobs.MemoryBlobStore; -import org.apache.jackrabbit.mk.fs.FileUtils; import org.apache.jackrabbit.mk.json.JsopReader; import org.apache.jackrabbit.mk.json.JsopStream; import org.apache.jackrabbit.mk.json.JsopTokenizer; import org.apache.jackrabbit.mk.json.JsopWriter; import org.apache.jackrabbit.mk.server.Server; -import org.apache.jackrabbit.mk.util.AscendingClock; import org.apache.jackrabbit.mk.util.Cache; import org.apache.jackrabbit.mk.util.CommitGate; -import org.apache.jackrabbit.mk.util.ExceptionFactory; -import org.apache.jackrabbit.mk.util.PathUtils; -import org.apache.jackrabbit.mk.wrapper.WrapperBase; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.plugins.index.old.mk.ExceptionFactory; +import org.apache.jackrabbit.oak.plugins.index.old.mk.wrapper.MicroKernelWrapperBase; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; /* @@ -60,9 +58,7 @@ /** * A simple MicroKernel implementation. */ -public class SimpleKernelImpl extends WrapperBase implements MicroKernel { - - private static final HashMap INSTANCES = new HashMap(); +public class SimpleKernelImpl extends MicroKernelWrapperBase implements MicroKernel { private static final int REV_SKIP_OFFSET = 20; @@ -78,7 +74,7 @@ public class SimpleKernelImpl extends WrapperBase implements MicroKernel { private Server server; private boolean disposed; - private SimpleKernelImpl(String name) { + public SimpleKernelImpl(String name) { this.name = name; boolean startServer = false; if (name.startsWith("server:")) { @@ -122,47 +118,6 @@ private SimpleKernelImpl(String name) { } } - /** - * Get or open the object. The following prefixes are supported: - *
  • fs: store the binaries in the file system - *
  • server: also start the server - *
- * - * @param url the url - * @return the object - */ - public static synchronized SimpleKernelImpl get(String url) { - boolean clean = false; - if (url.endsWith(";clean")) { - url = url.substring(0, url.length() - ";clean".length()); - clean = true; - } - url = url.replaceAll("\\{homeDir\\}", System.getProperty("homeDir", ".")); - if (clean) { - String dir = url.substring(url.lastIndexOf(':') + 1); - try { - FileUtils.deleteRecursive(dir, false); - } catch (Exception e) { - throw ExceptionFactory.convert(e); - } - } - String name; - if (url.startsWith("simple:")) { - name = url.substring("simple:".length()); - } else { - name = url.substring("mem:".length()); - } - if (clean) { - INSTANCES.remove(name); - } - SimpleKernelImpl instance = INSTANCES.get(name); - if (instance == null) { - instance = new SimpleKernelImpl(name); - INSTANCES.put(name, instance); - } - return instance; - } - private void applyConfig(NodeImpl head) { // /head/config doesn't always exist if (head.exists("config")) { @@ -173,7 +128,10 @@ private void applyConfig(NodeImpl head) { } } + @Override public synchronized String commitStream(String rootPath, JsopReader jsonDiff, String revisionId, String message) { + revisionId = revisionId == null ? headRevision : revisionId; + // TODO message should be json // TODO read / write version // TODO getJournal and getRevision don't have a path, @@ -193,7 +151,7 @@ private String doCommit(String rootPath, JsopReader t, String revisionId, String JsopWriter diff = new JsopStream(); while (true) { int r = t.read(); - if (r == JsopTokenizer.END) { + if (r == JsopReader.END) { break; } String path = PathUtils.concat(rootPath, t.readString()); @@ -229,7 +187,7 @@ private String doCommit(String rootPath, JsopReader t, String revisionId, String t.read(':'); boolean isConfigChange = from.startsWith(":root/head/config/"); String value; - if (t.matches(JsopTokenizer.NULL)) { + if (t.matches(JsopReader.NULL)) { value = null; diff.tag('^').key(path).value(null); } else { @@ -323,18 +281,20 @@ private String doCommit(String rootPath, JsopReader t, String revisionId, String break; } case '*': { - // TODO is it really required? // TODO possibly support target position notation - // TODO support copy in wrappers, index,... t.read(':'); String target = t.readString(); - diff.tag('*').key(path).value(target); if (!PathUtils.isAbsolute(target)) { target = PathUtils.concat(rootPath, target); } - NodeImpl node = data.getNode(from); + diff.tag('*').key(path).value(target); String to = PathUtils.relativize("/", target); - data = data.cloneAndAddChildNode(to, false, null, node, rev); + NodeImpl node = data.getNode(from); + JsopStream json = new JsopStream(); + node.append(json, Integer.MAX_VALUE, 0, Integer.MAX_VALUE, false); + json.read('{'); + NodeImpl n2 = NodeImpl.parse(nodeMap, json, rev); + data = data.cloneAndAddChildNode(to, false, null, n2, rev); break; } default: @@ -375,11 +335,13 @@ private NodeImpl getRoot() { return nodeMap.getRootId().getNode(nodeMap); } + @Override public String getHeadRevision() { return headRevision; } - public JsopReader getRevisionsStream(long since, int maxEntries) { + @Override + public JsopReader getRevisionsStream(long since, int maxEntries, String path) { NodeImpl node = getRoot(); long sinceNanos = since * 1000000; ArrayList revisions = new ArrayList(); @@ -417,11 +379,16 @@ public JsopReader getRevisionsStream(long since, int maxEntries) { return buff.endArray(); } - public String waitForCommit(String oldHeadRevision, long maxWaitMillis) throws InterruptedException { - return gate.waitForCommit(oldHeadRevision, maxWaitMillis); + @Override + public String waitForCommit(String oldHeadRevisionId, long maxWaitMillis) throws InterruptedException { + return gate.waitForCommit(oldHeadRevisionId, maxWaitMillis); } - public JsopReader getJournalStream(String fromRevisionId, String toRevisionId, String filter) { + @Override + public JsopReader getJournalStream(String fromRevisionId, String toRevisionId, String path) { + fromRevisionId = fromRevisionId == null ? headRevision : fromRevisionId; + toRevisionId = toRevisionId == null ? headRevision : toRevisionId; + long fromRev = Revision.parseId(fromRevisionId); long toRev = Revision.parseId(toRevisionId); NodeImpl node = getRoot(); @@ -455,7 +422,7 @@ public JsopReader getJournalStream(String fromRevisionId, String toRevisionId, S return buff.endArray(); } - private NodeImpl getRevisionNode(NodeImpl node, long fromRev, long toRev) { + private static NodeImpl getRevisionNode(NodeImpl node, long fromRev, long toRev) { while (true) { long next = -1; String nextRev = null; @@ -485,7 +452,10 @@ private NodeImpl getRevisionNode(NodeImpl node, long fromRev, long toRev) { } - public JsopReader diffStream(String fromRevisionId, String toRevisionId, String filter) { + @Override + public JsopReader diffStream(String fromRevisionId, String toRevisionId, String path, int depth) { + fromRevisionId = fromRevisionId == null ? headRevision : fromRevisionId; + toRevisionId = toRevisionId == null ? headRevision : toRevisionId; // TODO implement if required return new JsopStream(); } @@ -500,11 +470,10 @@ public JsopReader diffStream(String fromRevisionId, String toRevisionId, String * @param revisionId the revision * @return the json string */ - public JsopReader getNodesStream(String path, String revisionId) { - return getNodesStream(path, revisionId, 1, 0, -1, null); - } - + @Override public JsopReader getNodesStream(String path, String revisionId, int depth, long offset, int count, String filter) { + revisionId = revisionId == null ? headRevision : revisionId; + // TODO offset > 0 should mean the properties are not included if (count < 0) { count = nodeMap.getMaxMemoryChildren(); @@ -525,7 +494,7 @@ public JsopReader getNodesStream(String path, String revisionId, int depth, long n = getRevisionDataRoot(revisionId).getNode(path.substring(1)); } if (n == null) { - throw ExceptionFactory.get("Path not found: " + path); + return null; } JsopStream json = new JsopStream(); n.append(json, depth, offset, count, true); @@ -552,11 +521,19 @@ private NodeImpl getRevisionIfExists(String revisionId) { return head; } else { long rev = Revision.parseId(revisionId); - return getRevisionNode(node, rev, rev).getNode("head"); + NodeImpl rnode = getRevisionNode(node, rev, rev); + if (rnode != null) { + return rnode.getNode("head"); + } else { + return null; + } } } + @Override public boolean nodeExists(String path, String revisionId) { + revisionId = revisionId == null ? headRevision : revisionId; + if (!PathUtils.isAbsolute(path)) { throw ExceptionFactory.get("Not an absolute path: " + path); } @@ -568,23 +545,41 @@ public boolean nodeExists(String path, String revisionId) { return getRevisionDataRoot(revisionId).exists(path.substring(1)); } + @Override public long getChildNodeCount(String path, String revisionId) { + revisionId = revisionId == null ? headRevision : revisionId; + if (!PathUtils.isAbsolute(path)) { throw ExceptionFactory.get("Not an absolute path: " + path); } return getRevisionDataRoot(revisionId).getNode(path).getChildNodeCount(); } + @Override public long getLength(String blobId) { - return ds.getBlobLength(blobId); + try { + return ds.getBlobLength(blobId); + } catch (Exception e) { + throw ExceptionFactory.convert(e); + } } + @Override public int read(String blobId, long pos, byte[] buff, int off, int length) { - return ds.readBlob(blobId, pos, buff, off, length); + try { + return ds.readBlob(blobId, pos, buff, off, length); + } catch (Exception e) { + throw ExceptionFactory.convert(e); + } } + @Override public String write(InputStream in) { - return ds.writeBlob(in); + try { + return ds.writeBlob(in); + } catch (Exception e) { + throw ExceptionFactory.convert(e); + } } public synchronized void dispose() { @@ -592,8 +587,6 @@ public synchronized void dispose() { disposed = true; gate.commit("end"); nodeMap.close(); - ds.close(); - INSTANCES.remove(name); if (server != null) { server.stop(); server = null; @@ -601,8 +594,22 @@ public synchronized void dispose() { } } + @Override public String toString() { return "simple:" + name; } + @Override + public String branch(String trunkRevisionId) throws MicroKernelException { + trunkRevisionId = trunkRevisionId == null ? headRevision : trunkRevisionId; + + // TODO OAK-45 support + throw new UnsupportedOperationException(); + } + + @Override + public String merge(String branchRevisionId, String message) throws MicroKernelException { + // TODO OAK-45 support + throw new UnsupportedOperationException(); + } } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/StringCache.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/StringCache.java similarity index 97% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/util/StringCache.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/StringCache.java index 543262fe3a6..c63b122e459 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/StringCache.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/StringCache.java @@ -14,14 +14,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.util; +package org.apache.jackrabbit.oak.plugins.index.old.mk.simple; import java.lang.ref.SoftReference; +import org.apache.jackrabbit.mk.util.IOUtils; + /** * A simple string cache. */ -public class StringCache { +class StringCache { public static final boolean OBJECT_CACHE = getBooleanSetting("mk.objectCache", true); public static final int OBJECT_CACHE_SIZE = IOUtils.nextPowerOf2(getIntSetting("mk.objectCacheSize", 1024)); diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/BranchMergeMicroKernel.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/BranchMergeMicroKernel.java new file mode 100644 index 00000000000..f4d7d63af09 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/BranchMergeMicroKernel.java @@ -0,0 +1,219 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.old.mk.wrapper; + +import java.io.InputStream; +import java.util.HashSet; +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.api.MicroKernelException; + +/** + * A MicroKernel wrapper that provides limited support for branch and merge even + * if the underlying implementation does not support it. + */ +public class BranchMergeMicroKernel implements MicroKernel { + + private static final String TRUNK = "trunk"; + + private final MicroKernel base; + private final HashSet knownBranches = new HashSet(); + private int nextBranch; + private String trunkHeadRevision; + private String busyBranch; + + public BranchMergeMicroKernel(MicroKernel base) { + this.base = base; + } + + @Override + public synchronized String branch(String trunkRevisionId) { + if (trunkRevisionId == null) { + trunkRevisionId = getHeadRevision(); + } + String head = base.getHeadRevision(); + String branchId = getBranchId(trunkRevisionId); + if (!TRUNK.equals(branchId)) { + throw new MicroKernelException("Cannot branch off a branch: " + + trunkRevisionId); + } + trunkHeadRevision = head; + String branch = "b" + nextBranch++; + knownBranches.add(branch); + String branchRev = branch + "-" + getHeadRevision(); + busyBranch = null; + return branchRev; + } + + private synchronized boolean isKnownBranch(String branchId) { + if (TRUNK.equals(branchId)) { + return false; + } + return knownBranches.contains(branchId); + } + + private static String getBranchId(String revisionId) { + if (revisionId == null) { + return TRUNK; + } + int idx = revisionId.indexOf('-'); + if (idx <= 0) { + return TRUNK; + } + return revisionId.substring(0, idx); + } + + private static String getRevisionId(String branchId, String revisionId) { + if (TRUNK.equals(branchId)) { + return revisionId; + } + return revisionId.substring(branchId.length() + 1); + } + + private static String getCombinedRevisionId(String branchId, + String revisionId) { + if (TRUNK.equals(branchId)) { + return revisionId; + } + return branchId + "-" + revisionId; + } + + @Override + public synchronized String commit(String path, String jsonDiff, + String revisionId, String message) { + if (revisionId == null) { + revisionId = getHeadRevision(); + } + String branchId = getBranchId(revisionId); + // if another branch is active, wait until it's changes are merged + while (true) { + if (!TRUNK.equals(branchId) && !isKnownBranch(branchId)) { + throw new MicroKernelException("Unknown branch: " + revisionId); + } + if (busyBranch == null || branchId.equals(busyBranch)) { + break; + } + try { + wait(1000); + } catch (InterruptedException e) { + // ignore + } + } + busyBranch = branchId; + String rev = getRevisionId(branchId, revisionId); + String rev2 = base.commit(path, jsonDiff, rev, message); + return getCombinedRevisionId(branchId, rev2); + } + + @Override + public String diff(String fromRevisionId, String toRevisionId, String path, + int depth) { + String fromBranch = getBranchId(fromRevisionId); + String toBranch = getBranchId(toRevisionId); + String from = getRevisionId(fromBranch, fromRevisionId); + String to = getRevisionId(toBranch, toRevisionId); + return base.diff(from, to, path, depth); + } + + @Override + public long getChildNodeCount(String path, String revisionId) { + String branch = getBranchId(revisionId); + String rev = getRevisionId(branch, revisionId); + return base.getChildNodeCount(path, rev); + } + + @Override + public String getHeadRevision() { + if (trunkHeadRevision != null) { + return trunkHeadRevision; + } + return base.getHeadRevision(); + } + + @Override + public String getJournal(String fromRevisionId, String toRevisionId, + String path) { + String fromBranch = getBranchId(fromRevisionId); + String toBranch = getBranchId(toRevisionId); + if (!fromBranch.equals(TRUNK) || !toBranch.equals(TRUNK)) { + throw new MicroKernelException( + "This operation is not supported on branches: " + + fromRevisionId + " - " + toRevisionId); + } + return base.getJournal(fromRevisionId, toRevisionId, path); + } + + @Override + public long getLength(String blobId) { + return base.getLength(blobId); + } + + @Override + public String getNodes(String path, String revisionId, int depth, + long offset, int maxChildNodes, String filter) { + String branch = getBranchId(revisionId); + String rev = getRevisionId(branch, revisionId); + return base.getNodes(path, rev, depth, offset, maxChildNodes, filter); + } + + @Override + public String getRevisionHistory(long since, int maxEntries, String path) { + return base.getRevisionHistory(since, maxEntries, path); + } + + @Override + public String merge(String branchRevisionId, String message) { + String branch = getBranchId(branchRevisionId); + if (TRUNK.equals(branch)) { + throw new MicroKernelException("Can not merge the trunk"); + } + knownBranches.remove(branch); + busyBranch = null; + trunkHeadRevision = null; + return getHeadRevision(); + } + + @Override + public boolean nodeExists(String path, String revisionId) { + String branch = getBranchId(revisionId); + String rev = getRevisionId(branch, revisionId); + return base.nodeExists(path, rev); + } + + @Override + public int read(String blobId, long pos, byte[] buff, int off, int length) { + return base.read(blobId, pos, buff, off, length); + } + + @Override + public String waitForCommit(String oldHeadRevisionId, long timeout) + throws InterruptedException { + String branch = getBranchId(oldHeadRevisionId); + String rev = getRevisionId(branch, oldHeadRevisionId); + return base.waitForCommit(rev, timeout); + } + + @Override + public String write(InputStream in) { + return base.write(in); + } + + @Override + public String toString() { + return getClass().getName() + ":" + base; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/wrapper/LogWrapper.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/LogWrapper.java similarity index 79% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/wrapper/LogWrapper.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/LogWrapper.java index d1cb2551057..3435ce04612 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/wrapper/LogWrapper.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/LogWrapper.java @@ -14,14 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.wrapper; +package org.apache.jackrabbit.oak.plugins.index.old.mk.wrapper; import java.io.InputStream; import java.util.concurrent.atomic.AtomicInteger; -import org.apache.jackrabbit.mk.MicroKernelFactory; + import org.apache.jackrabbit.mk.api.MicroKernel; import org.apache.jackrabbit.mk.json.JsopBuilder; -import org.apache.jackrabbit.mk.util.ExceptionFactory; +import org.apache.jackrabbit.oak.plugins.index.old.mk.ExceptionFactory; /** * A logging microkernel implementation. @@ -38,14 +38,7 @@ public LogWrapper(MicroKernel mk) { this.mk = mk; } - public static synchronized LogWrapper get(String url) { - String u = url.substring("log:".length()); - LogWrapper w = new LogWrapper(MicroKernelFactory.getInstance(u)); - w.log("MicroKernel mk" + w.id + " = MicroKernelFactory.getInstance(" - + JsopBuilder.encode(u) + ");"); - return w; - } - + @Override public String commit(String path, String jsonDiff, String revisionId, String message) { try { logMethod("commit", path, jsonDiff, revisionId, message); @@ -59,16 +52,10 @@ public String commit(String path, String jsonDiff, String revisionId, String mes } public void dispose() { - try { - logMethod("dispose"); - mk.dispose(); - logResult(); - } catch (Exception e) { - logException(e); - throw convert(e); - } + // do nothing } + @Override public String getHeadRevision() { try { logMethod("getHeadRevision"); @@ -81,10 +68,11 @@ public String getHeadRevision() { } } - public String getJournal(String fromRevisionId, String toRevisionId, String filter) { + @Override + public String getJournal(String fromRevisionId, String toRevisionId, String path) { try { logMethod("getJournal", fromRevisionId, toRevisionId); - String result = mk.getJournal(fromRevisionId, toRevisionId, filter); + String result = mk.getJournal(fromRevisionId, toRevisionId, path); logResult(result); return result; } catch (Exception e) { @@ -93,10 +81,11 @@ public String getJournal(String fromRevisionId, String toRevisionId, String filt } } - public String diff(String fromRevisionId, String toRevisionId, String filter) { + @Override + public String diff(String fromRevisionId, String toRevisionId, String path, int depth) { try { - logMethod("diff", fromRevisionId, toRevisionId, filter); - String result = mk.diff(fromRevisionId, toRevisionId, filter); + logMethod("diff", fromRevisionId, toRevisionId, path); + String result = mk.diff(fromRevisionId, toRevisionId, path, depth); logResult(result); return result; } catch (Exception e) { @@ -105,6 +94,7 @@ public String diff(String fromRevisionId, String toRevisionId, String filter) { } } + @Override public long getLength(String blobId) { try { logMethod("getLength", blobId); @@ -117,10 +107,11 @@ public long getLength(String blobId) { } } - public String getNodes(String path, String revisionId) { + @Override + public String getNodes(String path, String revisionId, int depth, long offset, int maxChildNodes, String filter) { try { - logMethod("getNodes", path, revisionId); - String result = mk.getNodes(path, revisionId); + logMethod("getNodes", path, revisionId, depth, offset, maxChildNodes, filter); + String result = mk.getNodes(path, revisionId, depth, offset, maxChildNodes, filter); logResult(result); return result; } catch (Exception e) { @@ -129,22 +120,11 @@ public String getNodes(String path, String revisionId) { } } - public String getNodes(String path, String revisionId, int depth, long offset, int count, String filter) { + @Override + public String getRevisionHistory(long since, int maxEntries, String path) { try { - logMethod("getNodes", path, revisionId, depth, offset, count, filter); - String result = mk.getNodes(path, revisionId, depth, offset, count, filter); - logResult(result); - return result; - } catch (Exception e) { - logException(e); - throw convert(e); - } - } - - public String getRevisions(long since, int maxEntries) { - try { - logMethod("getRevisions", since, maxEntries); - String result = mk.getRevisions(since, maxEntries); + logMethod("getRevisionHistory", since, maxEntries, path); + String result = mk.getRevisionHistory(since, maxEntries, path); logResult(result); return result; } catch (Exception e) { @@ -153,6 +133,7 @@ public String getRevisions(long since, int maxEntries) { } } + @Override public boolean nodeExists(String path, String revisionId) { try { logMethod("nodeExists", path, revisionId); @@ -165,6 +146,7 @@ public boolean nodeExists(String path, String revisionId) { } } + @Override public long getChildNodeCount(String path, String revisionId) { try { logMethod("getChildNodeCount", path, revisionId); @@ -177,6 +159,7 @@ public long getChildNodeCount(String path, String revisionId) { } } + @Override public int read(String blobId, long pos, byte[] buff, int off, int length) { try { logMethod("read", blobId, pos, buff, off, length); @@ -189,10 +172,11 @@ public int read(String blobId, long pos, byte[] buff, int off, int length) { } } - public String waitForCommit(String oldHeadRevision, long maxWaitMillis) throws InterruptedException { + @Override + public String waitForCommit(String oldHeadRevisionId, long maxWaitMillis) throws InterruptedException { try { - logMethod("waitForCommit", oldHeadRevision, maxWaitMillis); - String result = mk.waitForCommit(oldHeadRevision, maxWaitMillis); + logMethod("waitForCommit", oldHeadRevisionId, maxWaitMillis); + String result = mk.waitForCommit(oldHeadRevisionId, maxWaitMillis); logResult(result); return result; } catch (InterruptedException e) { @@ -204,6 +188,7 @@ public String waitForCommit(String oldHeadRevision, long maxWaitMillis) throws I } } + @Override public String write(InputStream in) { try { logMethod("write", in.toString()); @@ -216,6 +201,32 @@ public String write(InputStream in) { } } + @Override + public String branch(String trunkRevisionId) { + try { + logMethod("branch", trunkRevisionId); + String result = mk.branch(trunkRevisionId); + logResult(result); + return result; + } catch (Exception e) { + logException(e); + throw convert(e); + } + } + + @Override + public String merge(String branchRevisionId, String message) { + try { + logMethod("merge", branchRevisionId, message); + String result = mk.merge(branchRevisionId, message); + logResult(result); + return result; + } catch (Exception e) { + logException(e); + throw convert(e); + } + } + private void logMethod(String methodName, Object... args) { StringBuilder buff = new StringBuilder("mk"); buff.append(id).append('.').append(methodName).append('('); @@ -238,7 +249,7 @@ public static String quote(Object o) { return o.toString(); } - private RuntimeException convert(Exception e) { + private static RuntimeException convert(Exception e) { if (e instanceof RuntimeException) { return (RuntimeException) e; } @@ -246,19 +257,15 @@ private RuntimeException convert(Exception e) { return ExceptionFactory.convert(e); } - private void logException(Exception e) { + private static void logException(Exception e) { log("// exception: " + e.toString()); } - private void logResult(Object result) { + private static void logResult(Object result) { log("// " + quote(result)); } - private void logResult() { - // ignored - } - - private void log(String message) { + private static void log(String message) { if (DEBUG) { System.out.println(message); } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/wrapper/Wrapper.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/MicroKernelWrapper.java similarity index 76% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/wrapper/Wrapper.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/MicroKernelWrapper.java index 7efbad5f1b3..55a8f392c37 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/wrapper/Wrapper.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/MicroKernelWrapper.java @@ -14,24 +14,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.wrapper; +package org.apache.jackrabbit.oak.plugins.index.old.mk.wrapper; import org.apache.jackrabbit.mk.api.MicroKernel; import org.apache.jackrabbit.mk.api.MicroKernelException; import org.apache.jackrabbit.mk.json.JsopReader; -public interface Wrapper extends MicroKernel { - - JsopReader getRevisionsStream(long since, int maxEntries) throws MicroKernelException; +/** + * This interface allows a MicroKernel client to use a JsopReader instead of + * having to use strings. + */ +public interface MicroKernelWrapper extends MicroKernel { - JsopReader getJournalStream(String fromRevisionId, String toRevisionId, String filter) throws MicroKernelException; + JsopReader getRevisionsStream(long since, int maxEntries, String path) throws MicroKernelException; - JsopReader getNodesStream(String path, String revisionId) throws MicroKernelException; + JsopReader getJournalStream(String fromRevisionId, String toRevisionId, String path) throws MicroKernelException; JsopReader getNodesStream(String path, String revisionId, int depth, long offset, int count, String filter) throws MicroKernelException; String commitStream(String path, JsopReader jsonDiff, String revisionId, String message) throws MicroKernelException; - JsopReader diffStream(String fromRevisionId, String toRevisionId, String filter); + JsopReader diffStream(String fromRevisionId, String toRevisionId, String path, int depth); } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/MicroKernelWrapperBase.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/MicroKernelWrapperBase.java new file mode 100644 index 00000000000..0e842f2a8c8 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/MicroKernelWrapperBase.java @@ -0,0 +1,210 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.old.mk.wrapper; + +import java.io.InputStream; +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.api.MicroKernelException; +import org.apache.jackrabbit.mk.json.JsopReader; +import org.apache.jackrabbit.mk.json.JsopTokenizer; + +/** + * A MicroKernel implementation that extends this interface can use a JsopReader + * instead of having to use strings. + */ +public abstract class MicroKernelWrapperBase implements MicroKernel, MicroKernelWrapper { + + @Override + public final String commit(String path, String jsonDiff, String revisionId, String message) { + return commitStream(path, new JsopTokenizer(jsonDiff), revisionId, message); + } + + @Override + public final String getJournal(String fromRevisionId, String toRevisionId, String path) { + return getJournalStream(fromRevisionId, toRevisionId, path).toString(); + } + + @Override + public final String getNodes(String path, String revisionId, int depth, long offset, int maxChildNodes, String filter) { + JsopReader reader = + getNodesStream(path, revisionId, depth, offset, maxChildNodes, filter); + if (reader != null) { + return reader.toString(); + } else { + return null; + } + } + + @Override + public final String diff(String fromRevisionId, String toRevisionId, String path, int depth) { + return diffStream(fromRevisionId, toRevisionId, path, depth).toString(); + } + + @Override + public final String getRevisionHistory(long since, int maxEntries, String path) { + return getRevisionsStream(since, maxEntries, path).toString(); + } + + /** + * Wrap a MicroKernel implementation so that the MicroKernelWrapper + * interface can be used. + * + * @param mk the MicroKernel implementation to wrap + * @return the wrapped instance + */ + public static MicroKernelWrapper wrap(final MicroKernel mk) { + if (mk instanceof MicroKernelWrapperImpl) { + return (MicroKernelWrapper) mk; + } + return new MicroKernelWrapperImpl(mk); + } + + /** + * Unwrap a wrapped MicroKernel implementation previously created by + * {@link #wrap}; + * + * @param wrapper the MicroKernel wrapper to unwrap + * @return the unwrapped instance + * @throws IllegalArgumentException if the specified instance was not + * originally returned by {@link #wrap}. + */ + public static MicroKernel unwrap(final MicroKernelWrapper wrapper) { + if (wrapper instanceof MicroKernelWrapperImpl) { + return ((MicroKernelWrapperImpl) wrapper).getWrapped(); + } + throw new IllegalArgumentException("wrapper instance was not created by this factory"); + } + + /** + * A wrapper for MicroKernel implementations that don't support JSOP methods. + */ + private static class MicroKernelWrapperImpl implements MicroKernelWrapper { + + final MicroKernel wrapped; + + MicroKernelWrapperImpl(MicroKernel mk) { + wrapped = mk; + } + + MicroKernel getWrapped() { + return wrapped; + } + + @Override + public String commitStream(String path, JsopReader jsonDiff, String revisionId, String message) { + return wrapped.commit(path, jsonDiff.toString(), revisionId, message); + } + + @Override + public JsopReader getJournalStream(String fromRevisionId, String toRevisionId, String path) { + return new JsopTokenizer(wrapped.getJournal(fromRevisionId, toRevisionId, path)); + } + + @Override + public JsopReader getNodesStream(String path, String revisionId, int depth, long offset, int count, String filter) { + String json = wrapped.getNodes( + path, revisionId, depth, offset, count, filter); + if (json != null) { + return new JsopTokenizer(json); + } else { + return null; + } + } + + @Override + public JsopReader getRevisionsStream(long since, int maxEntries, String path) { + return new JsopTokenizer(wrapped.getRevisionHistory(since, maxEntries, path)); + } + + @Override + public JsopReader diffStream(String fromRevisionId, String toRevisionId, String path, int depth) { + return new JsopTokenizer(wrapped.diff(fromRevisionId, toRevisionId, path, depth)); + } + + @Override + public String commit(String path, String jsonDiff, String revisionId, String message) { + return wrapped.commit(path, jsonDiff, revisionId, message); + } + + @Override + public String branch(String trunkRevisionId) { + return wrapped.branch(trunkRevisionId); + } + + @Override + public String merge(String branchRevisionId, String message) { + return wrapped.merge(branchRevisionId, message); + } + + @Override + public String diff(String fromRevisionId, String toRevisionId, String path, int depth) { + return wrapped.diff(fromRevisionId, toRevisionId, path, depth); + } + + @Override + public String getHeadRevision() throws MicroKernelException { + return wrapped.getHeadRevision(); + } + + @Override + public String getJournal(String fromRevisionId, String toRevisionId, String path) { + return wrapped.getJournal(fromRevisionId, toRevisionId, path); + } + + @Override + public long getLength(String blobId) { + return wrapped.getLength(blobId); + } + + @Override + public String getNodes(String path, String revisionId, int depth, long offset, int maxChildNodes, String filter) { + return wrapped.getNodes(path, revisionId, depth, offset, maxChildNodes, filter); + } + + @Override + public String getRevisionHistory(long since, int maxEntries, String path) { + return wrapped.getRevisionHistory(since, maxEntries, path); + } + + @Override + public boolean nodeExists(String path, String revisionId) { + return wrapped.nodeExists(path, revisionId); + } + + @Override + public long getChildNodeCount(String path, String revisionId) { + return wrapped.getChildNodeCount(path, revisionId); + } + + @Override + public int read(String blobId, long pos, byte[] buff, int off, int length) { + return wrapped.read(blobId, pos, buff, off, length); + } + + @Override + public String waitForCommit(String oldHeadRevisionId, long maxWaitMillis) throws InterruptedException { + return wrapped.waitForCommit(oldHeadRevisionId, maxWaitMillis); + } + + @Override + public String write(InputStream in) { + return wrapped.write(in); + } + + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/wrapper/SecurityWrapper.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/SecurityWrapper.java similarity index 82% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/wrapper/SecurityWrapper.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/SecurityWrapper.java index 90987c4b01e..0c7046016be 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/wrapper/SecurityWrapper.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/SecurityWrapper.java @@ -14,21 +14,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.wrapper; +package org.apache.jackrabbit.oak.plugins.index.old.mk.wrapper; -import java.io.InputStream; -import org.apache.jackrabbit.mk.MicroKernelFactory; import org.apache.jackrabbit.mk.api.MicroKernel; -import org.apache.jackrabbit.mk.api.MicroKernelException; import org.apache.jackrabbit.mk.json.JsopReader; import org.apache.jackrabbit.mk.json.JsopStream; import org.apache.jackrabbit.mk.json.JsopTokenizer; import org.apache.jackrabbit.mk.json.JsopWriter; -import org.apache.jackrabbit.mk.simple.NodeImpl; -import org.apache.jackrabbit.mk.simple.NodeMap; -import org.apache.jackrabbit.mk.util.ExceptionFactory; -import org.apache.jackrabbit.mk.util.PathUtils; import org.apache.jackrabbit.mk.util.SimpleLRUCache; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.plugins.index.old.mk.ExceptionFactory; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeImpl; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeMap; + +import java.io.InputStream; /** * A microkernel prototype implementation that filters nodes based on simple @@ -41,20 +40,42 @@ * This implementation is not meant for production, it is only used to find * (performance and other) problems when using such an approach. */ -public class SecurityWrapper extends WrapperBase implements MicroKernel { +public class SecurityWrapper extends MicroKernelWrapperBase implements MicroKernel { - private final Wrapper mk; + private final MicroKernelWrapper mk; private final boolean admin, write; private final String[] userRights; private final NodeMap map = new NodeMap(); private final SimpleLRUCache cache = SimpleLRUCache.newInstance(100); private String rightsRevision; - private SecurityWrapper(MicroKernel mk, String[] rights) { + /** + * Decorates the given {@link MicroKernel} with authentication and + * authorization. The responsibility of properly disposing the given + * MicroKernel instance remains with the caller. + * + * @param mk the wrapped kernel + * @param user the user name + * @param pass the password + */ + public SecurityWrapper(MicroKernel mk, String user, String pass) { + this.mk = MicroKernelWrapperBase.wrap(mk); // TODO security for the index mechanism - this.mk = WrapperBase.wrap(mk); + + String role = mk.getNodes("/:user/" + user, mk.getHeadRevision(), 1, 0, -1, null); + NodeMap map = new NodeMap(); + JsopReader t = new JsopTokenizer(role); + t.read('{'); + NodeImpl n = NodeImpl.parse(map, t, 0); + String password = JsopTokenizer.decodeQuoted(n.getProperty("password")); + if (!pass.equals(password)) { + throw ExceptionFactory.get("Wrong password"); + } + String[] rights = + JsopTokenizer.decodeQuoted(n.getProperty("rights")).split(","); this.userRights = rights; - boolean isAdmin = false, canWrite = false; + boolean isAdmin = false; + boolean canWrite = false; for (String r : rights) { if (r.equals("admin")) { isAdmin = true; @@ -62,43 +83,11 @@ private SecurityWrapper(MicroKernel mk, String[] rights) { canWrite = true; } } - admin = isAdmin; - write = canWrite; - } - - public static synchronized SecurityWrapper get(String url) { - String userPassUrl = url.substring("sec:".length()); - int index = userPassUrl.indexOf(':'); - if (index < 0) { - throw ExceptionFactory.get("Expected url format: sec:user@pass:"); - } - String u = userPassUrl.substring(index + 1); - String userPass = userPassUrl.substring(0, index); - index = userPass.indexOf('@'); - if (index < 0) { - throw ExceptionFactory.get("Expected url format: sec:user@pass:"); - } - String user = userPass.substring(0, index); - String pass = userPass.substring(index + 1); - MicroKernel mk = MicroKernelFactory.getInstance(u); - try { - String role = mk.getNodes("/:user/" + user, mk.getHeadRevision()); - NodeMap map = new NodeMap(); - JsopReader t = new JsopTokenizer(role); - t.read('{'); - NodeImpl n = NodeImpl.parse(map, t, 0); - String password = JsopTokenizer.decodeQuoted(n.getProperty("password")); - if (!pass.equals(password)) { - throw ExceptionFactory.get("Wrong password"); - } - String rights = JsopTokenizer.decodeQuoted(n.getProperty("rights")); - return new SecurityWrapper(mk, rights.split(",")); - } catch (MicroKernelException e) { - mk.dispose(); - throw e; - } + this.admin = isAdmin; + this.write = canWrite; } + @Override public String commitStream(String rootPath, JsopReader jsonDiff, String revisionId, String message) { checkRights(rootPath, true); if (!admin) { @@ -108,16 +97,18 @@ public String commitStream(String rootPath, JsopReader jsonDiff, String revision } public void dispose() { - mk.dispose(); + // do nothing } + @Override public String getHeadRevision() { return mk.getHeadRevision(); } - public JsopReader getJournalStream(String fromRevisionId, String toRevisionId, String filter) { + @Override + public JsopReader getJournalStream(String fromRevisionId, String toRevisionId, String path) { rightsRevision = getHeadRevision(); - JsopReader t = mk.getJournalStream(fromRevisionId, toRevisionId, filter); + JsopReader t = mk.getJournalStream(fromRevisionId, toRevisionId, path); if (admin) { return t; } @@ -158,9 +149,10 @@ public JsopReader getJournalStream(String fromRevisionId, String toRevisionId, S return buff; } - public JsopReader diffStream(String fromRevisionId, String toRevisionId, String path) { + @Override + public JsopReader diffStream(String fromRevisionId, String toRevisionId, String path, int depth) { rightsRevision = getHeadRevision(); - JsopReader diff = mk.diffStream(fromRevisionId, toRevisionId, path); + JsopReader diff = mk.diffStream(fromRevisionId, toRevisionId, path, depth); if (admin) { return diff; } @@ -176,7 +168,7 @@ private JsopReader filterDiff(JsopReader t, String revisionId) { private void verifyDiff(JsopReader t, String revisionId, String rootPath, JsopWriter diff) { while (true) { int r = t.read(); - if (r == JsopTokenizer.END) { + if (r == JsopReader.END) { break; } String path; @@ -217,7 +209,7 @@ private void verifyDiff(JsopReader t, String revisionId, String rootPath, JsopWr case '^': t.read(':'); String value; - if (t.matches(JsopTokenizer.NULL)) { + if (t.matches(JsopReader.NULL)) { if (checkDiff(path, diff)) { if (checkPropertyRights(path)) { diff.tag('^').key(path).value(null); @@ -287,21 +279,19 @@ private boolean checkDiff(String path, JsopWriter target) { return false; } + @Override public long getLength(String blobId) { return mk.getLength(blobId); } - public JsopReader getNodesStream(String path, String revisionId) { - return getNodesStream(path, revisionId, 1, 0, -1, null); - } - + @Override public JsopReader getNodesStream(String path, String revisionId, int depth, long offset, int count, String filter) { rightsRevision = getHeadRevision(); if (!checkRights(path, false)) { - throw ExceptionFactory.get("Node not found: " + path); + return null; } JsopReader t = mk.getNodesStream(path, revisionId, depth, offset, count, filter); - if (admin) { + if (admin || t == null) { return t; } t.read('{'); @@ -309,7 +299,7 @@ public JsopReader getNodesStream(String path, String revisionId, int depth, long n = filterAccess(path, n); JsopStream buff = new JsopStream(); if (n == null) { - throw ExceptionFactory.get("Node not found: " + path); + return null; } else { // TODO childNodeCount properties might be wrong // when count and offset are used @@ -318,10 +308,12 @@ public JsopReader getNodesStream(String path, String revisionId, int depth, long return buff; } - public JsopReader getRevisionsStream(long since, int maxEntries) { - return mk.getRevisionsStream(since, maxEntries); + @Override + public JsopReader getRevisionsStream(long since, int maxEntries, String path) { + return mk.getRevisionsStream(since, maxEntries, path); } + @Override public boolean nodeExists(String path, String revisionId) { rightsRevision = getHeadRevision(); if (!checkRights(path, false)) { @@ -330,6 +322,7 @@ public boolean nodeExists(String path, String revisionId) { return mk.nodeExists(path, revisionId); } + @Override public long getChildNodeCount(String path, String revisionId) { rightsRevision = getHeadRevision(); if (!checkRights(path, false)) { @@ -338,20 +331,35 @@ public long getChildNodeCount(String path, String revisionId) { return mk.getChildNodeCount(path, revisionId); } + @Override public int read(String blobId, long pos, byte[] buff, int off, int length) { return mk.read(blobId, pos, buff, off, length); } - public String waitForCommit(String oldHeadRevision, long maxWaitMillis) throws InterruptedException { - return mk.waitForCommit(oldHeadRevision, maxWaitMillis); + @Override + public String waitForCommit(String oldHeadRevisionId, long maxWaitMillis) throws InterruptedException { + return mk.waitForCommit(oldHeadRevisionId, maxWaitMillis); } + @Override public String write(InputStream in) { rightsRevision = getHeadRevision(); checkRights(null, true); return mk.write(in); } + @Override + public String branch(String trunkRevisionId) { + // TODO OAK-45 support + return mk.branch(trunkRevisionId); + } + + @Override + public String merge(String branchRevisionId, String message) { + // TODO OAK-45 support + return mk.merge(branchRevisionId, message); + } + private NodeImpl filterAccess(String path, NodeImpl n) { if (!checkRights(path, false)) { return null; @@ -377,7 +385,7 @@ private NodeImpl filterAccess(String path, NodeImpl n) { return n; } - private boolean checkPropertyRights(String path) { + private static boolean checkPropertyRights(String path) { return !PathUtils.getName(path).equals(":rights"); } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/wrapper/VirtualRepositoryWrapper.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/VirtualRepositoryWrapper.java similarity index 75% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/wrapper/VirtualRepositoryWrapper.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/VirtualRepositoryWrapper.java index cb672ec9176..5c227cecc18 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/wrapper/VirtualRepositoryWrapper.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/VirtualRepositoryWrapper.java @@ -14,23 +14,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.wrapper; +package org.apache.jackrabbit.oak.plugins.index.old.mk.wrapper; import java.io.InputStream; import java.util.HashMap; import java.util.TreeMap; import java.util.Map.Entry; -import org.apache.jackrabbit.mk.MicroKernelFactory; + import org.apache.jackrabbit.mk.api.MicroKernel; import org.apache.jackrabbit.mk.api.MicroKernelException; import org.apache.jackrabbit.mk.json.JsopBuilder; import org.apache.jackrabbit.mk.json.JsopReader; import org.apache.jackrabbit.mk.json.JsopTokenizer; import org.apache.jackrabbit.mk.json.JsopWriter; -import org.apache.jackrabbit.mk.simple.NodeImpl; -import org.apache.jackrabbit.mk.simple.NodeMap; -import org.apache.jackrabbit.mk.util.ExceptionFactory; -import org.apache.jackrabbit.mk.util.PathUtils; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.plugins.index.old.mk.ExceptionFactory; +import org.apache.jackrabbit.oak.plugins.index.old.mk.MicroKernelFactory; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeImpl; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeMap; /** * A microkernel prototype implementation that distributes nodes based on the path, @@ -38,15 +39,14 @@ * All mounted repositories contains the configuration as follows: * /:mount/rep1 { url: "mk:...", paths: "/a,/b" }. */ -public class VirtualRepositoryWrapper extends WrapperBase implements MicroKernel { +public class VirtualRepositoryWrapper extends MicroKernelWrapperBase implements MicroKernel { - private static final String PREFIX = "virtual:"; private static final String MOUNT = "/:mount"; /** * The 'main' (wrapped) implementation. */ - private final Wrapper mk; + private final MicroKernelWrapper mk; /** * Path map. @@ -64,7 +64,7 @@ public class VirtualRepositoryWrapper extends WrapperBase implements MicroKernel * Mount map. * Key: mount name, value: microkernel implementation. */ - private final HashMap mounts = new HashMap(); + private final HashMap mounts = new HashMap(); /** * Head revision map. @@ -74,53 +74,43 @@ public class VirtualRepositoryWrapper extends WrapperBase implements MicroKernel private final NodeMap map = new NodeMap(); - private VirtualRepositoryWrapper(MicroKernel mk) { - this.mk = WrapperBase.wrap(mk); - } - - public static synchronized VirtualRepositoryWrapper get(String url) { - String urlMeta = url.substring(PREFIX.length()); - MicroKernel mk = MicroKernelFactory.getInstance(urlMeta); - try { - String head = mk.getHeadRevision(); - VirtualRepositoryWrapper vm = new VirtualRepositoryWrapper(mk); - if (mk.nodeExists(MOUNT, head)) { - String mounts = mk.getNodes(MOUNT, head); - NodeMap map = new NodeMap(); - JsopReader t = new JsopTokenizer(mounts); - t.read('{'); - NodeImpl n = NodeImpl.parse(map, t, 0); - for (long pos = 0;; pos++) { - String childName = n.getChildNodeName(pos); - if (childName == null) { - break; - } - NodeImpl mount = n.getNode(childName); - String mountUrl = JsopTokenizer.decodeQuoted(mount.getProperty("url")); - String[] paths = JsopTokenizer.decodeQuoted(mount.getProperty("paths")).split(","); - vm.addMount(childName, mountUrl, paths); - vm.getHeadRevision(); + public VirtualRepositoryWrapper(MicroKernel mk) { + this.mk = MicroKernelWrapperBase.wrap(mk); + + String head = mk.getHeadRevision(); + if (mk.nodeExists(MOUNT, head)) { + String mounts = mk.getNodes(MOUNT, head, 1, 0, -1, null); + NodeMap map = new NodeMap(); + JsopReader t = new JsopTokenizer(mounts); + t.read('{'); + NodeImpl n = NodeImpl.parse(map, t, 0); + for (long pos = 0;; pos++) { + String childName = n.getChildNodeName(pos); + if (childName == null) { + break; } + NodeImpl mount = n.getNode(childName); + String mountUrl = JsopTokenizer.decodeQuoted(mount.getProperty("url")); + String[] paths = JsopTokenizer.decodeQuoted(mount.getProperty("paths")).split(","); + addMount(childName, mountUrl, paths); + getHeadRevision(); } - return vm; - } catch (MicroKernelException e) { - mk.dispose(); - throw e; } } private void addMount(String mount, String url, String[] paths) { MicroKernel mk = MicroKernelFactory.getInstance(url); - mounts.put(mount, WrapperBase.wrap(mk)); + mounts.put(mount, MicroKernelWrapperBase.wrap(mk)); for (String p : paths) { dir.put(p, mount); } } + @Override public String commitStream(String rootPath, JsopReader t, String revisionId, String message) { while (true) { int r = t.read(); - if (r == JsopTokenizer.END) { + if (r == JsopReader.END) { break; } String path = PathUtils.relativize("/", PathUtils.concat(rootPath, t.readString())); @@ -150,7 +140,7 @@ public String commitStream(String rootPath, JsopReader t, String revisionId, Str case '^': t.read(':'); String value; - if (t.matches(JsopTokenizer.NULL)) { + if (t.matches(JsopReader.NULL)) { JsopWriter diff = new JsopBuilder(); diff.tag('^').key(path).value(null); buffer(path, diff); @@ -238,16 +228,17 @@ private JsopWriter getBuilder(String mount) { } public void dispose() { - for (MicroKernel m : mounts.values()) { - m.dispose(); + for (MicroKernelWrapper wrapper : mounts.values()) { + MicroKernelFactory.disposeInstance(MicroKernelWrapperBase.unwrap(wrapper)); } } + @Override public String getHeadRevision() { StringBuilder buff = new StringBuilder(); if (revisions.size() != mounts.size()) { revisions.clear(); - for (Entry e : mounts.entrySet()) { + for (Entry e : mounts.entrySet()) { String m = e.getKey(); String r = e.getValue().getHeadRevision(); revisions.put(m, r); @@ -264,33 +255,34 @@ public String getHeadRevision() { return buff.toString(); } - public JsopReader getJournalStream(String fromRevisionId, String toRevisionId, String filter) { - return mk.getJournalStream(fromRevisionId, toRevisionId, filter); + @Override + public JsopReader getJournalStream(String fromRevisionId, String toRevisionId, String path) { + return mk.getJournalStream(fromRevisionId, toRevisionId, path); } - public JsopReader diffStream(String fromRevisionId, String toRevisionId, String path) throws MicroKernelException { - return mk.diffStream(fromRevisionId, toRevisionId, path); + @Override + public JsopReader diffStream(String fromRevisionId, String toRevisionId, String path, int depth) throws MicroKernelException { + return mk.diffStream(fromRevisionId, toRevisionId, path, depth); } + @Override public long getLength(String blobId) { return mk.getLength(blobId); } - public JsopReader getNodesStream(String path, String revisionId) { - return getNodesStream(path, revisionId, 1, 0, -1, null); - } - + @Override public JsopReader getNodesStream(String path, String revisionId, int depth, long offset, int count, String filter) { String mount = getMount(path); if (mount == null) { - throw ExceptionFactory.get("Not mapped: " + path); + // not mapped + return null; } String rev = getRevision(mount, revisionId); - Wrapper mk = mounts.get(mount); + MicroKernelWrapper mk = mounts.get(mount); return mk.getNodesStream(path, rev, depth, offset, count, filter); } - private String getRevision(String mount, String revisionId) { + private static String getRevision(String mount, String revisionId) { for (String rev : revisionId.split(",")) { if (rev.startsWith(mount + ":")) { return rev.substring(mount.length() + 1); @@ -299,10 +291,12 @@ private String getRevision(String mount, String revisionId) { throw ExceptionFactory.get("Unknown revision: " + revisionId + " mount: " + mount); } - public JsopReader getRevisionsStream(long since, int maxEntries) { - return mk.getRevisionsStream(since, maxEntries); + @Override + public JsopReader getRevisionsStream(long since, int maxEntries, String path) { + return mk.getRevisionsStream(since, maxEntries, path); } + @Override public boolean nodeExists(String path, String revisionId) { String mount = getMount(path); if (mount == null) { @@ -313,6 +307,7 @@ public boolean nodeExists(String path, String revisionId) { return mk.nodeExists(path, rev); } + @Override public long getChildNodeCount(String path, String revisionId) { String mount = getMount(path); if (mount == null) { @@ -323,17 +318,31 @@ public long getChildNodeCount(String path, String revisionId) { return mk.getChildNodeCount(path, rev); } + @Override public int read(String blobId, long pos, byte[] buff, int off, int length) { return mk.read(blobId, pos, buff, off, length); } - public String waitForCommit(String oldHeadRevision, long maxWaitMillis) throws InterruptedException { - return mk.waitForCommit(oldHeadRevision, maxWaitMillis); + @Override + public String waitForCommit(String oldHeadRevisionId, long maxWaitMillis) throws InterruptedException { + return mk.waitForCommit(oldHeadRevisionId, maxWaitMillis); } + @Override public String write(InputStream in) { return mk.write(in); } + @Override + public String branch(String trunkRevisionId) { + // TODO OAK-45 support + return mk.branch(trunkRevisionId); + } + + @Override + public String merge(String branchRevisionId, String message) { + // TODO OAK-45 support + return mk.merge(branchRevisionId, message); + } } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndex.java new file mode 100644 index 00000000000..27de2056b76 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndex.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.property; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.query.index.IndexRowImpl; +import org.apache.jackrabbit.oak.query.index.TraversingCursor; +import org.apache.jackrabbit.oak.spi.query.Cursor; +import org.apache.jackrabbit.oak.spi.query.Filter; +import org.apache.jackrabbit.oak.spi.query.Filter.PropertyRestriction; +import org.apache.jackrabbit.oak.spi.query.IndexRow; +import org.apache.jackrabbit.oak.spi.query.QueryIndex; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import com.google.common.base.Charsets; +import com.google.common.collect.Sets; + +import static org.apache.jackrabbit.oak.commons.PathUtils.isAbsolute; + +/** + * Provides a QueryIndex that does lookups against a property index + * + *

+ * To define a property index on a subtree you have to add an oak:index node. + * + * Under it follows the index definition node that: + *

    + *
  • must be of type oak:queryIndexDefinition
  • + *
  • must have the type property set to property
  • + *
  • contains the propertyNames property that indicates what property will be stored in the index
  • + *
+ *

+ *

+ * Optionally you can specify the uniqueness constraint on a property index by + * setting the unique flag to true. + *

+ * + *

+ * Note: propertyNames can be a list of properties, and it is optional.in case it is missing, the node name will be used as a property name reference value + *

+ * + *

+ * Note: reindex is a property that when set to true, triggers a full content reindex. + *

+ * + *
+ * 
+ * {
+ *     NodeBuilder index = root.child("oak:index");
+ *     index.child("uuid")
+ *         .setProperty("jcr:primaryType", "oak:queryIndexDefinition", Type.NAME)
+ *         .setProperty("type", "property")
+ *         .setProperty("propertyNames", "jcr:uuid")
+ *         .setProperty("unique", true)
+ *         .setProperty("reindex", true);
+ * }
+ * 
+ * 
+ * + * @see QueryIndex + * @see PropertyIndexLookup + */ +public class PropertyIndex implements QueryIndex { + + private static final int MAX_STRING_LENGTH = 100; // TODO: configurable + + static List encode(PropertyValue value) { + List values = new ArrayList(); + + for (String v : value.getValue(Type.STRINGS)) { + try { + if (v.length() > MAX_STRING_LENGTH) { + v = v.substring(0, MAX_STRING_LENGTH); + } + values.add(URLEncoder.encode(v, Charsets.UTF_8.name())); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("UTF-8 is unsupported", e); + } + } + return values; + } + + + //--------------------------------------------------------< QueryIndex >-- + + @Override + public String getIndexName() { + return "oak:index"; + } + + @Override + public double getCost(Filter filter, NodeState root) { + // TODO: proper cost calculation + return 1.0; + } + + @Override + public Cursor query(Filter filter, NodeState root) { + Set paths = null; + + PropertyIndexLookup lookup = new PropertyIndexLookup(root); + for (PropertyRestriction pr : filter.getPropertyRestrictions()) { + if (pr.firstIncluding && pr.lastIncluding + && pr.first.equals(pr.last) // TODO: range queries + && lookup.isIndexed(pr.propertyName, "/")) { // TODO: path + Set set = lookup.find(pr.propertyName, pr.first); + if (paths == null) { + paths = Sets.newHashSet(set); + } else { + paths.retainAll(set); + } + } + } + + if (paths != null) { + return new PathCursor(paths); + } else { + return new TraversingCursor(filter, root); + } + } + + @Override + public String getPlan(Filter filter, NodeState root) { + return "oak:index"; // TODO: better plans + } + + private static class PathCursor implements Cursor { + + private final Iterator iterator; + + private String path; + + public PathCursor(Collection paths) { + this.iterator = paths.iterator(); + } + + @Override + public boolean next() { + if (iterator.hasNext()) { + path = iterator.next(); + return true; + } else { + path = null; + return false; + } + } + + @Override + public IndexRow currentRow() { + // TODO support jcr:score and possibly rep:exceprt + return new IndexRowImpl(isAbsolute(path) ? path : "/" + path); + } + + } + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexDiff.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexDiff.java new file mode 100644 index 00000000000..a71e314b317 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexDiff.java @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.property; + +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NODE_TYPE; +import static org.apache.jackrabbit.oak.commons.PathUtils.concat; + +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeState; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; +import org.apache.jackrabbit.oak.spi.state.NodeStateUtils; + +/** + * {@link NodeStateDiff} implementation that extracts changes for the + * {@link PropertyIndexHook} to be applied on the {@link PropertyIndex} + * + * @see PropertyIndexHook + * + */ +class PropertyIndexDiff implements NodeStateDiff { + + private final PropertyIndexDiff parent; + + private final NodeBuilder node; + + private final String name; + + private String path; + + private final Map> updates; + + private PropertyIndexDiff( + PropertyIndexDiff parent, + NodeBuilder node, String name, String path, + Map> updates) { + this.parent = parent; + this.node = node; + this.name = name; + this.path = path; + this.updates = updates; + + if (node != null && isIndexNodeType(node.getProperty(JCR_PRIMARYTYPE))) { + update(node, name); + } + if (node != null && node.hasChildNode(INDEX_DEFINITIONS_NAME)) { + NodeBuilder index = node.child(INDEX_DEFINITIONS_NAME); + for (String indexName : index.getChildNodeNames()) { + update(index.child(indexName), indexName); + } + } + } + + public PropertyIndexDiff( + NodeBuilder root, Map> updates) { + this(null, root, null, "/", updates); + } + + public PropertyIndexDiff(PropertyIndexDiff parent, String name) { + this(parent, getChildNode(parent.node, name), + name, null, parent.updates); + } + + private static NodeBuilder getChildNode(NodeBuilder node, String name) { + if (node != null && node.hasChildNode(name)) { + return node.child(name); + } else { + return null; + } + } + + private String getPath() { + if (path == null) { // => parent != null + path = concat(parent.getPath(), name); + } + return path; + } + + private Iterable getIndexes(String name) { + List indexes = updates.get(name); + if (indexes != null) { + return indexes; + } else { + return ImmutableList.of(); + } + } + + private void update(NodeBuilder builder, String indexName) { + PropertyState ps = builder.getProperty("propertyNames"); + Iterable propertyNames = ps != null ? ps.getValue(Type.STRINGS) + : ImmutableList.of(indexName); + for (String pname : propertyNames) { + List list = this.updates.get(pname); + if (list == null) { + list = Lists.newArrayList(); + this.updates.put(pname, list); + } + list.add(new PropertyIndexUpdate(getPath(), builder)); + } + } + + private boolean isIndexNodeType(PropertyState ps) { + return ps != null && !ps.isArray() + && ps.getValue(Type.STRING).equals(INDEX_DEFINITIONS_NODE_TYPE); + } + + //-----------------------------------------------------< NodeStateDiff >-- + + @Override + public void propertyAdded(PropertyState after) { + for (PropertyIndexUpdate update : getIndexes(after.getName())) { + update.insert(getPath(), after); + } + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) { + for (PropertyIndexUpdate update : getIndexes(after.getName())) { + update.remove(getPath(), before); + update.insert(getPath(), after); + } + } + + @Override + public void propertyDeleted(PropertyState before) { + for (PropertyIndexUpdate update : getIndexes(before.getName())) { + update.remove(getPath(), before); + } + } + + @Override + public void childNodeAdded(String name, NodeState after) { + childNodeChanged(name, MemoryNodeState.EMPTY_NODE, after); + } + + @Override + public void childNodeChanged( + String name, NodeState before, NodeState after) { + if (!NodeStateUtils.isHidden(name)) { + after.compareAgainstBaseState(before, new PropertyIndexDiff(this, name)); + } + } + + @Override + public void childNodeDeleted(String name, NodeState before) { + childNodeChanged(name, before, MemoryNodeState.EMPTY_NODE); + } + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexHook.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexHook.java new file mode 100644 index 00000000000..1caf83f2f9c --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexHook.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.plugins.index.property; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.plugins.index.IndexHook; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; + +import com.google.common.collect.Maps; + +/** + * {@link IndexHook} implementation that is responsible for keeping the + * {@link PropertyIndex} up to date + * + * @see PropertyIndex + * @see PropertyIndexLookup + * + */ +public class PropertyIndexHook implements IndexHook { + + private final NodeBuilder builder; + + private Map> indexes; + + public PropertyIndexHook(NodeBuilder builder) { + this.builder = builder; + } + + + // -----------------------------------------------------< IndexHook >-- + + @Override @Nonnull + public NodeStateDiff preProcess() { + indexes = Maps.newHashMap(); + return new PropertyIndexDiff(builder, indexes); + } + + @Override + public void postProcess() throws CommitFailedException { + for (List updates : indexes.values()) { + for (PropertyIndexUpdate update : updates) { + update.apply(); + } + } + } + + @Override + public void close() throws IOException { + indexes = null; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexHookProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexHookProvider.java new file mode 100644 index 00000000000..55748731463 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexHookProvider.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.plugins.index.property; + +import java.util.List; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.apache.jackrabbit.oak.plugins.index.IndexHook; +import org.apache.jackrabbit.oak.plugins.index.IndexHookProvider; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; + +import com.google.common.collect.ImmutableList; + +/** + * Service that provides PropertyIndex based IndexHooks + * + * @see PropertyIndexHook + * @see IndexHookProvider + * + */ +@Component +@Service(IndexHookProvider.class) +public class PropertyIndexHookProvider implements IndexHookProvider { + + private static final String TYPE = "property"; + + @Override + public List getIndexHooks(String type, + NodeBuilder builder) { + if (TYPE.equals(type)) { + return ImmutableList.of(new PropertyIndexHook(builder)); + } + return ImmutableList.of(); + } + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexLookup.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexLookup.java new file mode 100644 index 00000000000..5ab840d5a30 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexLookup.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.property; + +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME; + +import java.util.Set; + +import javax.annotation.Nullable; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; +import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import com.google.common.collect.Sets; + +/** + * Is responsible for querying the property index content. + * + *

+ * This class can be used directly on a subtree where there is an index defined + * by supplying a {@link NodeState} root. + *

+ * + *
+ * 
+ * {
+ *     NodeState state = ... // get a node state
+ *     PropertyIndexLookup lookup = new PropertyIndexLookup(state);
+ *     Set hits = lookup.find("foo", PropertyValues.newString("xyz"));
+ * }
+ * 
+ * 
+ */ +public class PropertyIndexLookup { + + private final NodeState root; + + public PropertyIndexLookup(NodeState root) { + this.root = root; + } + + /** + * Checks whether the named properties are indexed somewhere + * along the given path. + * + * @param name property name + * @param path lookup path + */ + public boolean isIndexed(String name, String path) { + if (getIndexDefinitionNode(name) != null) { + return true; + } + + if (path.startsWith("/")) { + path = path.substring(1); + } + int slash = path.indexOf('/'); + if (slash == -1) { + return false; + } + + NodeState child = root.getChildNode(path.substring(0, slash)); + return new PropertyIndexLookup(child).isIndexed( + name, path.substring(slash)); + } + + /** + * Searches for a given String value within this index. + * + *

Note if the property you are looking for is not of type String, the converted key value might not match the index key, and there will be no hits on the index.

+ * + * @param name + * the property name + * @param value + * the property value + * @return the set of matched paths + */ + public Set find(String name, String value) { + return find(name, PropertyValues.newString(value)); + } + + /** + * Searches for a given value within this index. + * + * @param name the property name + * @param value the property value + * @return the set of matched paths + */ + public Set find(String name, PropertyValue value) { + Set paths = Sets.newHashSet(); + + PropertyState property; + NodeState state = getIndexDefinitionNode(name); + if (state != null && state.getChildNode(":index") != null) { + state = state.getChildNode(":index"); + for (String p : PropertyIndex.encode(value)) { + property = state.getProperty(p); + if (property != null) { + // We have an entry for this value, so use it + for (String path : property.getValue(Type.STRINGS)) { + paths.add(path); + } + } + } + } else { + // No index available, so first check this node for a match + property = root.getProperty(name); + if (property != null) { + if (value.isArray()) { + // let query engine handle multi-valued look ups + // simply return all nodes that have this property + paths.add(""); + } else { + // does it match any of the values of this property? + for (int i = 0; i < property.count(); i++) { + if (property.getValue(value.getType(), i).equals(value.getValue(value.getType()))) { + paths.add(""); + // no need to check for more matches in this property + break; + } + } + } + } + + // ... and then recursively look up from the rest of the tree + for (ChildNodeEntry entry : root.getChildNodeEntries()) { + String base = entry.getName(); + PropertyIndexLookup lookup = + new PropertyIndexLookup(entry.getNodeState()); + for (String path : lookup.find(name, value)) { + if (path.isEmpty()) { + paths.add(base); + } else { + paths.add(base + "/" + path); + } + } + } + } + + return paths; + } + + @Nullable + private NodeState getIndexDefinitionNode(String name) { + NodeState state = root.getChildNode(INDEX_DEFINITIONS_NAME); + if (state != null) { + for (ChildNodeEntry entry : state.getChildNodeEntries()) { + PropertyState names = entry.getNodeState().getProperty("propertyNames"); + if (names != null) { + for (int i = 0; i < names.count(); i++) { + if (name.equals(names.getValue(Type.STRING, i))) { + return entry.getNodeState(); + } + } + } + } + } + return null; + } + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexProvider.java new file mode 100644 index 00000000000..68585c147ef --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexProvider.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.plugins.index.property; + +import java.util.List; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.spi.query.QueryIndex; +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import com.google.common.collect.ImmutableList; + +/** + * A provider for property indexes. + * + * @see PropertyIndex + * + */ +public class PropertyIndexProvider implements QueryIndexProvider { + + @Override @Nonnull + public List getQueryIndexes(NodeState state) { + return ImmutableList.of(new PropertyIndex()); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexUpdate.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexUpdate.java new file mode 100644 index 00000000000..b11769d8a3c --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexUpdate.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.property; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.jcr.PropertyType; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.memory.MemoryPropertyBuilder; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.PropertyBuilder; + +/** + * Takes care of applying the updates to the index content. + * + */ +class PropertyIndexUpdate { + + private final String path; + + private final NodeBuilder node; + + private final Map> insert; + + private final Map> remove; + + public PropertyIndexUpdate(String path, NodeBuilder node) { + this.path = path; + this.node = node; + this.insert = Maps.newHashMap(); + this.remove = Maps.newHashMap(); + } + + public void insert(String path, PropertyState value) { + Preconditions.checkArgument(path.startsWith(this.path)); + putValues(insert, path.substring(this.path.length()), value); + } + + public void remove(String path, PropertyState value) { + Preconditions.checkArgument(path.startsWith(this.path)); + putValues(remove, path.substring(this.path.length()), value); + } + + private static void putValues(Map> map, String path, + PropertyState value) { + if (value.getType().tag() != PropertyType.BINARY) { + List keys = PropertyIndex.encode(PropertyValues.create(value)); + for (String key : keys) { + Set paths = map.get(key); + if (paths == null) { + paths = Sets.newHashSet(); + map.put(key, paths); + } + paths.add(path); + } + } + } + + public void apply() throws CommitFailedException { + boolean unique = node.getProperty("unique") != null + && node.getProperty("unique").getValue(Type.BOOLEAN); + NodeBuilder index = node.child(":index"); + + for (Map.Entry> entry : remove.entrySet()) { + String encoded = entry.getKey(); + Set paths = entry.getValue(); + PropertyState property = index.getProperty(encoded); + if (property != null) { + PropertyBuilder builder = MemoryPropertyBuilder.create(Type.STRING, encoded); + for (String value : property.getValue(Type.STRINGS)) { + if (!paths.contains(value)) { + builder.addValue(value); + } + } + if (builder.isEmpty()) { + index.removeProperty(encoded); + } else { + index.setProperty(builder.getPropertyState(true)); + } + } + } + + for (Map.Entry> entry : insert.entrySet()) { + String encoded = entry.getKey(); + Set paths = entry.getValue(); + PropertyState property = index.getProperty(encoded); + PropertyBuilder builder = MemoryPropertyBuilder.create(Type.STRING) + .setName(encoded) + .assignFrom(property); + for (String path : paths) { + if (!builder.hasValue(path)) { + builder.addValue(path); + } + } + if (builder.isEmpty()) { + index.removeProperty(encoded); + } else if (unique && builder.count() > 1) { + throw new CommitFailedException( + "Uniqueness constraint violated"); + } else { + index.setProperty(builder.getPropertyState(true)); + } + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/AbstractBlob.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/AbstractBlob.java new file mode 100644 index 00000000000..e2b8641e114 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/AbstractBlob.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +import com.google.common.base.Optional; +import com.google.common.hash.HashCode; +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; +import com.google.common.io.ByteStreams; + +import org.apache.jackrabbit.oak.api.Blob; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Abstract base class for {@link Blob} implementations. + * This base class provides default implementations for + * {@code hashCode} and {@code equals}. + */ +public abstract class AbstractBlob implements Blob { + private static final Logger log = LoggerFactory.getLogger(AbstractBlob.class); + + private Optional hashCode = Optional.absent(); + + /** + * This hash code implementation returns the hash code of the underlying stream + * @return + */ + @Override + public int hashCode() { + return calculateSha256().asInt(); + } + + @Override + public byte[] sha256() { + return calculateSha256().asBytes(); + } + + private HashCode calculateSha256() { + // Blobs are immutable so we can safely cache the hash + if (!hashCode.isPresent()) { + InputStream is = getNewStream(); + try { + try { + Hasher hasher = Hashing.sha256().newHasher(); + byte[] buf = new byte[0x1000]; + int r; + while((r = is.read(buf)) != -1) { + hasher.putBytes(buf, 0, r); + } + hashCode = Optional.of(hasher.hash()); + } + catch (IOException e) { + log.warn("Error while hashing stream", e); + } + } + finally { + close(is); + } + } + return hashCode.get(); + } + + /** + * To {@code Blob} instances are considered equal iff they have the same SHA-256 hash code + * are equal. + * @param other + * @return + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof Blob)) { + return false; + } + + Blob that = (Blob) other; + return Arrays.equals(sha256(), that.sha256()); + } + + private static void close(InputStream s) { + try { + s.close(); + } + catch (IOException e) { + log.warn("Error while closing stream", e); + } + } + + @Override + public int compareTo(Blob o) { + return compare(getNewStream(), o.getNewStream()) ? 0 : 1; + } + + private static boolean compare(InputStream in2, InputStream in1) { + try { + try { + byte[] buf1 = new byte[0x1000]; + byte[] buf2 = new byte[0x1000]; + + while (true) { + int read1 = ByteStreams.read(in1, buf1, 0, 0x1000); + int read2 = ByteStreams.read(in2, buf2, 0, 0x1000); + if (read1 != read2 || !Arrays.equals(buf1, buf2)) { + return false; + } else if (read1 != 0x1000) { + return true; + } + } + } finally { + in1.close(); + in2.close(); + } + } catch (IOException e) { + return false; + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/ArrayBasedBlob.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/ArrayBasedBlob.java new file mode 100644 index 00000000000..e3abe0bdb02 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/ArrayBasedBlob.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.plugins.memory; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +import javax.annotation.Nonnull; + +import com.google.common.base.Charsets; + +/** + * This {@code Blob} implementations is based on an array of bytes. + */ +public class ArrayBasedBlob extends AbstractBlob { + private final byte[] value; + + public ArrayBasedBlob(byte[] value) { + this.value = value; + } + + @Override + public String toString() { + return new String(value, Charsets.UTF_8); + } + + @Nonnull + @Override + public InputStream getNewStream() { + return new ByteArrayInputStream(value); + } + + @Override + public long length() { + return value.length; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/BinaryPropertyState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/BinaryPropertyState.java new file mode 100644 index 00000000000..d0fef632085 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/BinaryPropertyState.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import javax.jcr.Value; + +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.value.Conversions; +import org.apache.jackrabbit.oak.plugins.value.Conversions.Converter; + +public class BinaryPropertyState extends SinglePropertyState { + private final Blob value; + + public BinaryPropertyState(String name, Blob value) { + super(name); + this.value = value; + } + + /** + * Create a {@code PropertyState} from an array of bytes. + * @param name The name of the property state + * @param value The value of the property state + * @return The new property state of type {@link Type#BINARY} + */ + public static PropertyState binaryProperty(String name, byte[] value) { + return new BinaryPropertyState(name, new ArrayBasedBlob(value)); + } + + /** + * Create a {@code PropertyState} from an array of bytes. + * @param name The name of the property state + * @param value The value of the property state + * @return The new property state of type {@link Type#BINARY} + */ + public static PropertyState binaryProperty(String name, String value) { + return new BinaryPropertyState(name, new StringBasedBlob(value)); + } + + /** + * Create a {@code PropertyState} from a {@link Blob}. + * @param name The name of the property state + * @param value The value of the property state + * @return The new property state of type {@link Type#BINARY} + */ + public static PropertyState binaryProperty(String name, Blob value) { + return new BinaryPropertyState(name, value); + } + + /** + * Create a {@code PropertyState} from a {@link javax.jcr.Value}. + * @param name The name of the property state + * @param value The value of the property state + * @return The new property state of type {@link Type#BINARY} + */ + public static PropertyState binaryProperty(String name, Value value) { + return new BinaryPropertyState(name, new ValueBasedBlob(value)); + } + + @Override + public Blob getValue() { + return value; + } + + @Override + public Converter getConverter() { + return Conversions.convert(value); + } + + @Override + public long size() { + return value.length(); + } + + @Override + public Type getType() { + return Type.BINARY; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/BooleanPropertyState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/BooleanPropertyState.java new file mode 100644 index 00000000000..a240f99c068 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/BooleanPropertyState.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.plugins.memory; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.value.Conversions; +import org.apache.jackrabbit.oak.plugins.value.Conversions.Converter; + +import static org.apache.jackrabbit.oak.api.Type.BOOLEAN; + +public class BooleanPropertyState extends SinglePropertyState { + private final boolean value; + + public BooleanPropertyState(String name, boolean value) { + super(name); + this.value = value; + } + + /** + * Create a {@code PropertyState} from a boolean. + * @param name The name of the property state + * @param value The value of the property state + * @return The new property state of type {@link Type#BOOLEAN} + */ + public static PropertyState booleanProperty(String name, boolean value) { + return new BooleanPropertyState(name, value); + } + + @Override + public Boolean getValue() { + return value; + } + + @Override + public Converter getConverter() { + return Conversions.convert(value); + } + + @Override + public Type getType() { + return BOOLEAN; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/DecimalPropertyState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/DecimalPropertyState.java new file mode 100644 index 00000000000..84356e42a75 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/DecimalPropertyState.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.plugins.memory; + +import java.math.BigDecimal; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.value.Conversions; +import org.apache.jackrabbit.oak.plugins.value.Conversions.Converter; + +import static org.apache.jackrabbit.oak.api.Type.DECIMAL; + +public class DecimalPropertyState extends SinglePropertyState { + private final BigDecimal value; + + public DecimalPropertyState(String name, BigDecimal value) { + super(name); + this.value = value; + } + + /** + * Create a {@code PropertyState} from a decimal. + * @param name The name of the property state + * @param value The value of the property state + * @return The new property state of type {@link Type#DECIMAL} + */ + public static PropertyState decimalProperty(String name, BigDecimal value) { + return new DecimalPropertyState(name, value); + } + + @Override + public BigDecimal getValue() { + return value; + } + + @Override + public Converter getConverter() { + return Conversions.convert(value); + } + + @Override + public Type getType() { + return DECIMAL; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/DoublePropertyState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/DoublePropertyState.java new file mode 100644 index 00000000000..756ecfa890f --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/DoublePropertyState.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.plugins.memory; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.value.Conversions; +import org.apache.jackrabbit.oak.plugins.value.Conversions.Converter; + +import static org.apache.jackrabbit.oak.api.Type.DOUBLE; + +public class DoublePropertyState extends SinglePropertyState { + private final double value; + + public DoublePropertyState(String name, double value) { + super(name); + this.value = value; + } + + /** + * Create a {@code PropertyState} from a double. + * @param name The name of the property state + * @param value The value of the property state + * @return The new property state of type {@link Type#DOUBLE} + */ + public static PropertyState doubleProperty(String name, double value) { + return new DoublePropertyState(name, value); + } + + @Override + public Double getValue() { + return value; + } + + @Override + public Converter getConverter() { + return Conversions.convert(value); + } + + @Override + public Type getType() { + return DOUBLE; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/EmptyPropertyState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/EmptyPropertyState.java new file mode 100644 index 00000000000..0d76942b08f --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/EmptyPropertyState.java @@ -0,0 +1,191 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import java.util.Collections; + +import javax.annotation.Nonnull; +import javax.jcr.PropertyType; + +import com.google.common.collect.Iterables; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.apache.jackrabbit.oak.api.Type.BINARIES; +import static org.apache.jackrabbit.oak.api.Type.STRING; +import static org.apache.jackrabbit.oak.api.Type.STRINGS; + +/** + * Abstract base class for {@link PropertyState} implementations + * providing default implementation which correspond to a property + * without any value. + */ +public abstract class EmptyPropertyState implements PropertyState { + private final String name; + + /** + * Create a new property state with the given {@code name} + * @param name The name of the property state. + */ + protected EmptyPropertyState(String name) { + this.name = name; + } + + /** + * Create an empty {@code PropertyState} + * @param name The name of the property state + * @param type The type of the property state + * @return The new property state + */ + public static PropertyState emptyProperty(String name, final Type type) { + if (!type.isArray()) { + throw new IllegalArgumentException("Not an array type:" + type); + } + return new EmptyPropertyState(name) { + @Override + public Type getType() { + return type; + } + }; + } + + @Nonnull + @Override + public String getName() { + return name; + } + + /** + * @return {@code true} + */ + @Override + public boolean isArray() { + return true; + } + + /** + * @return An empty list if {@code type.isArray()} is {@code true}. + * @throws IllegalArgumentException {@code type.isArray()} is {@code false}. + */ + @SuppressWarnings("unchecked") + @Nonnull + @Override + public T getValue(Type type) { + checkArgument(type.isArray(), "Type must be an array type"); + return (T) Collections.emptyList(); + } + + /** + * @throws IndexOutOfBoundsException always + */ + @Nonnull + @Override + public T getValue(Type type, int index) { + throw new IndexOutOfBoundsException(String.valueOf(index)); + } + + /** + * @throws IllegalStateException always + */ + @Override + public long size() { + throw new IllegalStateException("Not a single valued property"); + } + + /** + * @throws IndexOutOfBoundsException always + */ + @Override + public long size(int index) { + throw new IndexOutOfBoundsException(String.valueOf(index)); + } + + /** + * @return {@code 0} + */ + @Override + public int count() { + return 0; + } + + //------------------------------------------------------------< Object >-- + + /** + * Checks whether the given object is equal to this one. Two property + * states are considered equal if their names and types match and + * their string representation of their values are equal. + * Subclasses may override this method with a more efficient + * equality check if one is available. + * + * @param other target of the comparison + * @return {@code true} if the objects are equal, {@code false} otherwise + */ + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + else if (other instanceof PropertyState) { + PropertyState that = (PropertyState) other; + if (!getName().equals(that.getName())) { + return false; + } + if (!getType().equals(that.getType())) { + return false; + } + if (getType().tag() == PropertyType.BINARY) { + return Iterables.elementsEqual( + getValue(BINARIES), that.getValue(BINARIES)); + } + else { + return Iterables.elementsEqual( + getValue(STRINGS), that.getValue(STRINGS)); + } + } + else { + return false; + } + } + + /** + * Returns a hash code that's compatible with how the + * {@link #equals(Object)} method is implemented. The current + * implementation simply returns the hash code of the property name + * since {@link PropertyState} instances are not intended for use as + * hash keys. + * + * @return hash code + */ + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public String toString() { + if (isArray()) { + return getName() + '=' + getValue(STRINGS); + } + else { + return getName() + '=' + getValue(STRING); + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/GenericPropertyState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/GenericPropertyState.java new file mode 100644 index 00000000000..6fd3a9be0c7 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/GenericPropertyState.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.value.Conversions; +import org.apache.jackrabbit.oak.plugins.value.Conversions.Converter; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.apache.jackrabbit.oak.api.Type.NAME; +import static org.apache.jackrabbit.oak.api.Type.PATH; +import static org.apache.jackrabbit.oak.api.Type.REFERENCE; +import static org.apache.jackrabbit.oak.api.Type.URI; +import static org.apache.jackrabbit.oak.api.Type.WEAKREFERENCE; + +public class GenericPropertyState extends SinglePropertyState { + private final String value; + private final Type type; + + /** + * @throws IllegalArgumentException if {@code type.isArray()} is {@code true} + */ + public GenericPropertyState(String name, String value, Type type) { + super(name); + checkArgument(!type.isArray()); + this.value = value; + this.type = type; + } + + /** + * Create a {@code PropertyState} from a name. No validation is performed + * on the string passed for {@code value}. + * @param name The name of the property state + * @param value The value of the property state + * @return The new property state of type {@link Type#NAME} + */ + public static PropertyState nameProperty(String name, String value) { + return new GenericPropertyState(name, value, NAME); + } + + /** + * Create a {@code PropertyState} from a path. No validation is performed + * on the string passed for {@code value}. + * @param name The name of the property state + * @param value The value of the property state + * @return The new property state of type {@link Type#PATH} + */ + public static PropertyState pathProperty(String name, String value) { + return new GenericPropertyState(name, value, PATH); + } + + /** + * Create a {@code PropertyState} from a reference. No validation is performed + * on the string passed for {@code value}. + * @param name The name of the property state + * @param value The value of the property state + * @return The new property state of type {@link Type#REFERENCE} + */ + public static PropertyState referenceProperty(String name, String value) { + return new GenericPropertyState(name, value, REFERENCE); + } + + /** + * Create a {@code PropertyState} from a weak reference. No validation is performed + * on the string passed for {@code value}. + * @param name The name of the property state + * @param value The value of the property state + * @return The new property state of type {@link Type#WEAKREFERENCE} + */ + public static PropertyState weakreferenceProperty(String name, String value) { + return new GenericPropertyState(name, value, WEAKREFERENCE); + } + + /** + * Create a {@code PropertyState} from a URI. No validation is performed + * on the string passed for {@code value}. + * @param name The name of the property state + * @param value The value of the property state + * @return The new property state of type {@link Type#URI} + */ + public static PropertyState uriProperty(String name, String value) { + return new GenericPropertyState(name, value, URI); + } + + @Override + public String getValue() { + return value; + } + + @Override + public Converter getConverter() { + return Conversions.convert(value); + } + + @Override + public Type getType() { + return type; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/LongPropertyState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/LongPropertyState.java new file mode 100644 index 00000000000..2187096ed7d --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/LongPropertyState.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import java.util.Calendar; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.value.Conversions; +import org.apache.jackrabbit.oak.plugins.value.Conversions.Converter; + +public class LongPropertyState extends SinglePropertyState { + private final long value; + private final Type type; + + public LongPropertyState(String name, long value, Type type) { + super(name); + this.value = value; + this.type = type; + } + + /** + * Create a {@code PropertyState} from a long. + * @param name The name of the property state + * @param value The value of the property state + * @return The new property state of type {@link Type#LONG} + */ + public static PropertyState createLongProperty(String name, long value) { + return new LongPropertyState(name, value, Type.LONG); + } + + /** + * Create a {@code PropertyState} for a date value from a long. + * @param name The name of the property state + * @param value The value of the property state + * @return The new property state of type {@link Type#DATE} + */ + public static PropertyState createDateProperty(String name, long value) { + return new LongPropertyState(name, value, Type.DATE); + } + + /** + * Create a {@code PropertyState} for a date. + * @param name The name of the property state + * @param value The value of the property state + * @return The new property state of type {@link Type#DATE} + */ + public static PropertyState createDateProperty(String name, Calendar value) { + return new LongPropertyState(name, Conversions.convert(value).toLong(), Type.DATE); + } + + /** + * Create a {@code PropertyState} for a date from a String. + * @param name The name of the property state + * @param value The value of the property state + * @return The new property state of type {@link Type#DATE} + * @throws IllegalArgumentException if {@code value} is not a parseable to a date. + */ + public static PropertyState createDateProperty(String name, String value) { + return createDateProperty(name, Conversions.convert(value).toCalendar()); + } + + @Override + public Long getValue() { + return value; + } + + @Override + public Converter getConverter() { + if (type == Type.DATE) { + return Conversions.convert(Conversions.convert(value).toCalendar()); + } + else { + return Conversions.convert(value); + } + } + + @Override + public Type getType() { + return type; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MemoryChildNodeEntry.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MemoryChildNodeEntry.java new file mode 100644 index 00000000000..3e5993c7462 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MemoryChildNodeEntry.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.plugins.memory; + +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.jackrabbit.oak.spi.state.AbstractChildNodeEntry; +import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import com.google.common.base.Function; +import com.google.common.collect.Iterables; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Basic JavaBean implementation of a child node entry. + */ +public class MemoryChildNodeEntry extends AbstractChildNodeEntry { + + public static Iterable iterable( + Iterable> set) { + return Iterables.transform( + set, + new Function, ChildNodeEntry>() { + @Override + public ChildNodeEntry apply(Entry input) { + return new MemoryChildNodeEntry(input); + } + }); + } + + private final String name; + + private final NodeState node; + + /** + * Creates a child node entry with the given name and referenced + * child node state. + * + * @param name child node name + * @param node child node state + */ + public MemoryChildNodeEntry(String name, NodeState node) { + this.name = checkNotNull(name); + this.node = checkNotNull(node); + } + + /** + * Utility constructor that copies the name and referenced + * child node state from the given map entry. + * + * @param entry map entry + */ + public MemoryChildNodeEntry(Map.Entry entry) { + this(entry.getKey(), entry.getValue()); + } + + @Override + public String getName() { + return name; + } + + @Override + public NodeState getNodeState() { + return node; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MemoryNodeBuilder.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MemoryNodeBuilder.java new file mode 100644 index 00000000000..e96c9f34832 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MemoryNodeBuilder.java @@ -0,0 +1,525 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import java.util.Iterator; +import java.util.Map; + +import javax.annotation.Nonnull; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.spi.state.AbstractNodeState; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.apache.jackrabbit.oak.plugins.memory.ModifiedNodeState.with; +import static org.apache.jackrabbit.oak.plugins.memory.ModifiedNodeState.withNodes; +import static org.apache.jackrabbit.oak.plugins.memory.ModifiedNodeState.withProperties; + +/** + * In-memory node state builder. + *

+ * TODO: The following description is somewhat out of date + *

+ * The following two builder states are used + * to properly track uncommitted chances without relying on weak references + * or requiring hard references on the entire accessed subtree: + *

+ *
unmodified
+ *
+ * A child builder with no content changes starts in this state. + * It keeps a reference to the parent builder and knows it's name for + * use when connecting. Before each access the unconnected builder + * checks the parent for relevant changes to connect to. As long as + * there are no such changes, the builder remains unconnected and + * uses the immutable base state to respond to any content accesses. + *
+ *
connected
+ *
+ * Once a child node is first modified, it switches it's internal + * state from the immutable base state to a mutable one and records + * a hard reference to that state in the mutable parent state. After + * that the parent reference is cleared and no more state checks are + * made. Any other builder instances that refer to the same child node + * will update their internal states to point to that same mutable + * state instance and thus become connected at next access. + * A root state builder is always connected. + *
+ *
+ */ +public class MemoryNodeBuilder implements NodeBuilder { + + private static final NodeState NULL_STATE = new MemoryNodeState( + ImmutableMap.of(), + ImmutableMap.of()); + + /** + * Parent builder, or {@code null} for a root builder. + */ + private final MemoryNodeBuilder parent; + + /** + * Name of this child node within the parent builder, + * or {@code null} for a root builder. + */ + private final String name; + + /** + * Root builder, or {@code this} for the root itself. + */ + private final MemoryNodeBuilder root; + + /** + * Internal revision counter that is incremented in the root builder + * whenever anything changes in the tree below. Each builder instance + * has its own copy of the revision counter, for quickly checking whether + * any state changes are needed. + */ + private long revision; + + /** + * The base state of this builder, or {@code null} if this builder + * represents a new node that didn't yet exist in the base content tree. + */ + private NodeState baseState; + + /** + * The shared mutable state of connected builder instances, or + * {@code null} until this builder has been connected. + */ + private MutableNodeState writeState; + + /** + * Creates a new in-memory node state builder. + * + * @param parent parent node state builder + * @param name name of this node + */ + protected MemoryNodeBuilder(MemoryNodeBuilder parent, String name) { + this.parent = checkNotNull(parent); + this.name = checkNotNull(name); + + this.root = parent.root; + this.revision = -1; + + this.baseState = null; + this.writeState = null; + } + + /** + * Creates a new in-memory node state builder. + * + * @param base base state of the new builder + */ + public MemoryNodeBuilder(@Nonnull NodeState base) { + this.parent = null; + this.name = null; + + this.root = this; + this.revision = 0; + + this.baseState = checkNotNull(base); + this.writeState = new MutableNodeState(baseState); + } + + private NodeState read() { + if (revision != root.revision) { + assert(parent != null); // root never gets here + parent.read(); + + // The builder could have been reset, need to re-get base state + if (parent.baseState != null) { + baseState = parent.baseState.getChildNode(name); + } else { + baseState = null; + } + + // ... same for the write state + if (parent.writeState != null) { + writeState = parent.writeState.nodes.get(name); + if (writeState == null + && parent.writeState.nodes.containsKey(name)) { + throw new IllegalStateException( + "This node has been removed"); + } + } else { + writeState = null; + } + + revision = root.revision; + } + if (writeState != null) { + return writeState; + } else if (baseState != null) { + return baseState; + } else { + return NULL_STATE; + } + } + + private MutableNodeState write() { + return write(root.revision + 1); + } + + private MutableNodeState write(long newRevision) { + if (writeState == null || revision != root.revision) { + assert(parent != null); // root never gets here + parent.write(newRevision); + + // The builder could have been reset, need to re-get base state + if (parent.baseState != null) { + baseState = parent.baseState.getChildNode(name); + } else { + baseState = null; + } + + assert parent.writeState != null; // we just called parent.write() + writeState = parent.writeState.nodes.get(name); + if (writeState == null) { + if (parent.writeState.nodes.containsKey(name)) { + throw new IllegalStateException( + "This node has been removed"); + } else { + // need to make this node writable + NodeState base = baseState; + if (base == null) { + base = NULL_STATE; + } + writeState = new MutableNodeState(base); + parent.writeState.nodes.put(name, writeState); + } + } + } + revision = newRevision; + return writeState; + } + + /** + * Factory method for creating new child state builders. Subclasses may + * override this method to control the behavior of child state builders. + * + * @return new builder + */ + protected MemoryNodeBuilder createChildBuilder(String name) { + return new MemoryNodeBuilder(this, name); + } + + /** + * Called whenever this node is modified, i.e. a property is + * added, changed or removed, or a child node is added or removed. Changes + * inside child nodes or the subtrees below are not reported. The default + * implementation does nothing, but subclasses may override this method + * to better track changes. + */ + protected void updated() { + // do nothing + } + + protected void compareAgainstBaseState(NodeStateDiff diff) { + NodeState state = read(); + if (writeState != null) { + writeState.compareAgainstBaseState(state, diff); + } + } + + //--------------------------------------------------------< NodeBuilder >--- + + @Override + public NodeState getNodeState() { + read(); + if (writeState != null) { + return writeState.snapshot(); + } else { + assert baseState != null; // guaranteed by read() + return baseState; + } + } + + @Override + public NodeState getBaseState() { + return baseState; + } + + @Override + public void reset(NodeState newBase) { + if (this == root) { + baseState = checkNotNull(newBase); + writeState = new MutableNodeState(baseState); + revision++; + } else { + throw new IllegalStateException("Cannot reset a non-root builder"); + } + } + + @Override + public long getChildNodeCount() { + return read().getChildNodeCount(); + } + + @Override + public boolean hasChildNode(String name) { + return read().hasChildNode(name); + } + + @Override + public Iterable getChildNodeNames() { + return read().getChildNodeNames(); + } + + @Override @Nonnull + public NodeBuilder setNode(String name, NodeState state) { + write(); + + MutableNodeState childState = writeState.nodes.get(name); + if (childState == null) { + writeState.nodes.remove(name); + childState = createChildBuilder(name).write(); + } + childState.reset(state); + + updated(); + return this; + } + + @Override @Nonnull + public NodeBuilder removeNode(String name) { + MutableNodeState mstate = write(); + + if (mstate.base.getChildNode(name) != null) { + mstate.nodes.put(name, null); + } else { + mstate.nodes.remove(name); + } + + updated(); + return this; + } + + @Override + public long getPropertyCount() { + return read().getPropertyCount(); + } + + @Override + public Iterable getProperties() { + return read().getProperties(); + } + + + @Override + public PropertyState getProperty(String name) { + return read().getProperty(name); + } + + @Override @Nonnull + public NodeBuilder removeProperty(String name) { + MutableNodeState mstate = write(); + + if (mstate.base.getProperty(name) != null) { + mstate.properties.put(name, null); + } else { + mstate.properties.remove(name); + } + + updated(); + return this; + } + + @Override + public NodeBuilder setProperty(PropertyState property) { + MutableNodeState mstate = write(); + mstate.properties.put(property.getName(), property); + updated(); + return this; + } + + @Override + public NodeBuilder setProperty(String name, T value) { + setProperty(PropertyStates.createProperty(name, value)); + return this; + } + + @Override + public NodeBuilder setProperty(String name, T value, Type type) { + setProperty(PropertyStates.createProperty(name, value, type)); + return this; + } + + @Override + public NodeBuilder child(String name) { + read(); // shortcut when dealing with a read-only child node + if (baseState != null + && baseState.hasChildNode(name) + && (writeState == null || !writeState.nodes.containsKey(name))) { + return createChildBuilder(name); + } + + // no read-only child node found, switch to write mode + write(); + assert writeState != null; // guaranteed by write() + + NodeState childBase = null; + if (baseState != null) { + childBase = baseState.getChildNode(name); + } + + if (writeState.nodes.get(name) == null) { + if (writeState.nodes.containsKey(name)) { + // The child node was removed earlier and we're creating + // a new child with the same name. Use the null state to + // prevent the previous child state from re-surfacing. + childBase = null; + } + writeState.nodes.put(name, new MutableNodeState(childBase)); + } + + MemoryNodeBuilder builder = createChildBuilder(name); + builder.write(); + return builder; + } + + /** + * The mutable state being built. Instances of this class + * are never passed beyond the containing {@code MemoryNodeBuilder}, + * so it's not a problem that we intentionally break the immutability + * assumption of the {@link NodeState} interface. + */ + private class MutableNodeState extends AbstractNodeState { + + /** + * The immutable base state. + */ + private NodeState base; + + /** + * Set of added, modified or removed ({@code null} value) + * property states. + */ + private final Map properties = + Maps.newHashMap(); + + /** + * Set of added, modified or removed ({@code null} value) + * child nodes. + */ + private final Map nodes = + Maps.newHashMap(); + + public MutableNodeState(NodeState base) { + if (base != null) { + this.base = base; + } else { + this.base = MemoryNodeBuilder.NULL_STATE; + } + } + + public NodeState snapshot() { + Map nodes = Maps.newHashMap(); + for (Map.Entry entry : this.nodes.entrySet()) { + String name = entry.getKey(); + MutableNodeState node = entry.getValue(); + NodeState before = base.getChildNode(name); + if (node == null) { + if (before != null) { + nodes.put(name, null); + } + } else { + NodeState after = node.snapshot(); + if (after != before) { + nodes.put(name, after); + } + } + } + return with(base, Maps.newHashMap(this.properties), nodes); + } + + void reset(NodeState newBase) { + base = newBase; + properties.clear(); + + Iterator> iterator = + nodes.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + MutableNodeState cstate = entry.getValue(); + NodeState cbase = newBase.getChildNode(entry.getKey()); + if (cbase == null || cstate == null) { + iterator.remove(); + } else { + cstate.reset(cbase); + } + } + } + + //-----------------------------------------------------< NodeState >-- + + @Override + public long getPropertyCount() { + return withProperties(base, properties).getPropertyCount(); + } + + @Override + public PropertyState getProperty(String name) { + return withProperties(base, properties).getProperty(name); + } + + @Override @Nonnull + public Iterable getProperties() { + Map copy = Maps.newHashMap(properties); + return withProperties(base, copy).getProperties(); + } + + @Override + public long getChildNodeCount() { + return withNodes(base, nodes).getChildNodeCount(); + } + + @Override + public boolean hasChildNode(String name) { + return withNodes(base, nodes).hasChildNode(name); + } + + @Override + public NodeState getChildNode(String name) { + return withNodes(base, nodes).getChildNode(name); // mutable + } + + @Override @Nonnull + public Iterable getChildNodeNames() { + Map copy = Maps.newHashMap(nodes); + return withNodes(base, copy).getChildNodeNames(); + } + + @Override + public void compareAgainstBaseState(NodeState base, NodeStateDiff diff) { + with(this.base, properties, nodes).compareAgainstBaseState(base, diff); + } + + @Override @Nonnull + public NodeBuilder builder() { + throw new UnsupportedOperationException(); + } + + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MemoryNodeState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MemoryNodeState.java new file mode 100644 index 00000000000..2d4006dde5e --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MemoryNodeState.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.state.AbstractNodeState; +import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Basic in-memory node state implementation. + */ +public class MemoryNodeState extends AbstractNodeState { + + /** + * Singleton instance of an empty node state, i.e. one with neither + * properties nor child nodes. + */ + public static final NodeState EMPTY_NODE = new MemoryNodeState( + Collections.emptyMap(), + Collections.emptyMap()); + + private final Map properties; + + private final Map nodes; + + /** + * Creates a new node state with the given properties and child nodes. + * The given maps are stored as references, so their contents and + * iteration order must remain unmodified at least for as long as this + * node state instance is in use. + * + * @param properties properties + * @param nodes child nodes + */ + public MemoryNodeState( + Map properties, + Map nodes) { + assert Collections.disjoint(properties.keySet(), nodes.keySet()); + this.properties = properties; + this.nodes = nodes; + } + + @Override + public PropertyState getProperty(String name) { + return properties.get(name); + } + + @Override + public long getPropertyCount() { + return properties.size(); + } + + @Override + public Iterable getProperties() { + return properties.values(); + } + + @Override + public boolean hasChildNode(String name) { + return nodes.containsKey(name); + } + + @Override + public NodeState getChildNode(String name) { + return nodes.get(name); + } + + @Override + public long getChildNodeCount() { + return nodes.size(); + } + + @Override + public Iterable getChildNodeEntries() { + return MemoryChildNodeEntry.iterable(nodes.entrySet()); + } + + @Override + public NodeBuilder builder() { + return new MemoryNodeBuilder(this); + } + + /** + * We don't keep track of a separate base node state for + * {@link MemoryNodeState} instances, so this method will just do + * a generic diff against the given state. + */ + @Override + public void compareAgainstBaseState(NodeState base, NodeStateDiff diff) { + Map newProperties = + new HashMap(properties); + for (PropertyState before : base.getProperties()) { + PropertyState after = newProperties.remove(before.getName()); + if (after == null) { + diff.propertyDeleted(before); + } else if (!after.equals(before)) { + diff.propertyChanged(before, after); + } + } + for (PropertyState after : newProperties.values()) { + diff.propertyAdded(after); + } + + Map newNodes = + new HashMap(nodes); + for (ChildNodeEntry entry : base.getChildNodeEntries()) { + String name = entry.getName(); + NodeState before = entry.getNodeState(); + NodeState after = newNodes.remove(name); + if (after == null) { + diff.childNodeDeleted(name, before); + } else if (!after.equals(before)) { + diff.childNodeChanged(name, before, after); + } + } + for (Map.Entry entry : newNodes.entrySet()) { + diff.childNodeAdded(entry.getKey(), entry.getValue()); + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MemoryNodeStore.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MemoryNodeStore.java new file mode 100644 index 00000000000..267217435f2 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MemoryNodeStore.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.atomic.AtomicReference; + +import com.google.common.io.ByteStreams; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStore; +import org.apache.jackrabbit.oak.spi.state.NodeStoreBranch; + +/** + * Basic in-memory node store implementation. Useful as a base class for + * more complex functionality. + */ +public class MemoryNodeStore implements NodeStore { + + private final AtomicReference root = + new AtomicReference(MemoryNodeState.EMPTY_NODE); + + @Override + public NodeState getRoot() { + return root.get(); + } + + @Override + public NodeStoreBranch branch() { + return new MemoryNodeStoreBranch(root.get()); + } + + /** + * @return An instance of {@link ArrayBasedBlob}. + */ + @Override + public ArrayBasedBlob createBlob(InputStream inputStream) throws IOException { + try { + return new ArrayBasedBlob(ByteStreams.toByteArray(inputStream)); + } + finally { + inputStream.close(); + } + } + + private class MemoryNodeStoreBranch implements NodeStoreBranch { + + private final NodeState base; + + private volatile NodeState root; + + public MemoryNodeStoreBranch(NodeState base) { + this.base = base; + this.root = base; + } + + @Override + public NodeState getBase() { + return base; + } + + @Override + public NodeState getRoot() { + return root; + } + + @Override + public void setRoot(NodeState newRoot) { + this.root = newRoot; + } + + @Override + public NodeState merge() throws CommitFailedException { + while (!MemoryNodeStore.this.root.compareAndSet(base, root)) { + // TODO: rebase(); + throw new UnsupportedOperationException(); + } + return root; + } + + @Override + public boolean copy(String source, String target) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean move(String source, String target) { + throw new UnsupportedOperationException(); + } + + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MemoryPropertyBuilder.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MemoryPropertyBuilder.java new file mode 100644 index 00000000000..6af9e4ebb5f --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MemoryPropertyBuilder.java @@ -0,0 +1,260 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import java.math.BigDecimal; +import java.util.List; + +import javax.annotation.Nonnull; +import javax.jcr.PropertyType; + +import com.google.common.collect.Lists; +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.spi.state.PropertyBuilder; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +/** + * {@code PropertyBuilder} for building in memory {@code PropertyState} instances. + * @param + */ +public class MemoryPropertyBuilder implements PropertyBuilder { + private final Type type; + + private String name; + private List values = Lists.newArrayList(); + + /** + * Create a new instance for building {@code PropertyState} instances + * of the given {@code type}. + * @param type type of the {@code PropertyState} instances to be built. + * @throws IllegalArgumentException if {@code type.isArray()} is {@code true}. + */ + public MemoryPropertyBuilder(Type type) { + checkArgument(!type.isArray(), "type must not be array"); + this.type = type; + } + + /** + * Create a new instance for building {@code PropertyState} instances + * of the given {@code type}. + * @param type type of the {@code PropertyState} instances to be built. + * @return {@code PropertyBuilder} for {@code type} + */ + public static PropertyBuilder create(Type type) { + return new MemoryPropertyBuilder(type); + } + + /** + * Create a new instance for building {@code PropertyState} instances + * of the given {@code type}. The builder is initialised with the name and + * the values of {@code property}. + * Equivalent to + *
+     *     MemoryPropertyBuilder.create(type).assignFrom(property);
+     * 
+ * @param type type of the {@code PropertyState} instances to be built. + * @param property initial name and values + * @return {@code PropertyBuilder} for {@code type} + */ + public static PropertyBuilder create(Type type, PropertyState property) { + return create(type).assignFrom(property); + } + + /** + * Create a new instance for building {@code PropertyState} instances + * of the given {@code type}. The builder is initialised with the + * given {@code name}. + * Equivalent to + *
+     *     MemoryPropertyBuilder.create(type).setName(name);
+     * 
+ * @param type type of the {@code PropertyState} instances to be built. + * @param name initial name + * @return {@code PropertyBuilder} for {@code type} + */ + public static PropertyBuilder create(Type type, String name) { + return create(type).setName(name); + } + + @Override + public String getName() { + return name; + } + + @Override + public T getValue() { + return values.isEmpty() ? null : values.get(0); + } + + @Nonnull + @Override + public List getValues() { + return Lists.newArrayList(values); + } + + @Override + public T getValue(int index) { + return values.get(index); + } + + @Override + public boolean hasValue(Object value) { + return values.contains(value); + } + + @Override + public int count() { + return values.size(); + } + + @Override + public boolean isArray() { + return count() != 1; + } + + @Override + public boolean isEmpty() { + return count() == 0; + } + + @Nonnull + @Override + public PropertyState getPropertyState() { + return getPropertyState(false); + } + + @SuppressWarnings("unchecked") + @Nonnull + @Override + public PropertyState getPropertyState(boolean asArray) { + checkState(name != null, "Property has no name"); + if (values.isEmpty()) { + return EmptyPropertyState.emptyProperty(name, Type.fromTag(type.tag(), true)); + } + else if (isArray() || asArray) { + switch (type.tag()) { + case PropertyType.STRING: + return MultiStringPropertyState.stringProperty(name, (Iterable) values); + case PropertyType.BINARY: + return MultiBinaryPropertyState.binaryPropertyFromBlob(name, (Iterable) values); + case PropertyType.LONG: + return MultiLongPropertyState.createLongProperty(name, (Iterable) values); + case PropertyType.DOUBLE: + return MultiDoublePropertyState.doubleProperty(name, (Iterable) values); + case PropertyType.DATE: + return MultiLongPropertyState.createDateProperty(name, (Iterable) values); + case PropertyType.BOOLEAN: + return MultiBooleanPropertyState.booleanProperty(name, (Iterable) values); + case PropertyType.DECIMAL: + return MultiDecimalPropertyState.decimalProperty(name, (Iterable) values); + default: + return new MultiGenericPropertyState(name, (Iterable) values, Type.fromTag(type.tag(), true)); + } + } + else { + T value = values.get(0); + switch (type.tag()) { + case PropertyType.STRING: + return StringPropertyState.stringProperty(name, (String) value); + case PropertyType.BINARY: + return BinaryPropertyState.binaryProperty(name, (Blob) value); + case PropertyType.LONG: + return LongPropertyState.createLongProperty(name, (Long) value); + case PropertyType.DOUBLE: + return DoublePropertyState.doubleProperty(name, (Double) value); + case PropertyType.DATE: + return LongPropertyState.createDateProperty(name, (String) value); + case PropertyType.BOOLEAN: + return BooleanPropertyState.booleanProperty(name, (Boolean) value); + case PropertyType.DECIMAL: + return DecimalPropertyState.decimalProperty(name, (BigDecimal) value); + default: + return new GenericPropertyState(name, (String) value, type); + } + } + } + + @SuppressWarnings("unchecked") + @Nonnull + @Override + public PropertyBuilder assignFrom(PropertyState property) { + if (property != null) { + setName(property.getName()); + if (property.isArray()) { + setValues((Iterable) property.getValue(type.getArrayType())); + } + else { + setValue(property.getValue(type)); + } + } + return this; + } + + @Nonnull + @Override + public PropertyBuilder setName(String name) { + this.name = name; + return this; + } + + @Nonnull + @Override + public PropertyBuilder setValue(T value) { + values.clear(); + values.add(value); + return this; + } + + @Nonnull + @Override + public PropertyBuilder addValue(T value) { + values.add(value); + return this; + } + + @Nonnull + @Override + public PropertyBuilder setValue(T value, int index) { + values.set(index, value); + return this; + } + + @Nonnull + @Override + public PropertyBuilder setValues(Iterable values) { + this.values = Lists.newArrayList(values); + return this; + } + + @Nonnull + @Override + public PropertyBuilder removeValue(int index) { + values.remove(index); + return this; + } + + @Nonnull + @Override + public PropertyBuilder removeValue(Object value) { + values.remove(value); + return this; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/ModifiedNodeState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/ModifiedNodeState.java new file mode 100644 index 00000000000..769a93040d3 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/ModifiedNodeState.java @@ -0,0 +1,340 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Predicates.in; +import static com.google.common.base.Predicates.not; +import static com.google.common.base.Predicates.notNull; +import static com.google.common.collect.Collections2.filter; +import static com.google.common.collect.Iterables.concat; +import static com.google.common.collect.Iterables.filter; +import static com.google.common.collect.Maps.filterValues; +import static org.apache.jackrabbit.oak.plugins.memory.MemoryChildNodeEntry.iterable; + +import java.util.Map; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.state.AbstractNodeState; +import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; + +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; + +/** + * Immutable snapshot of a mutable node state. + */ +public class ModifiedNodeState extends AbstractNodeState { + + public static NodeState withProperties( + NodeState base, Map properties) { + if (properties.isEmpty()) { + return base; + } else { + return new ModifiedNodeState( + base, properties, ImmutableMap.of()); + } + } + + public static NodeState withNodes( + NodeState base, Map nodes) { + if (nodes.isEmpty()) { + return base; + } else { + return new ModifiedNodeState( + base, ImmutableMap.of(), nodes); + } + } + + public static NodeState with( + NodeState base, + Map properties, + Map nodes) { + if (properties.isEmpty() && nodes.isEmpty()) { + return base; + } else { + return new ModifiedNodeState(base, properties, nodes); + } + } + + public static ModifiedNodeState collapse(ModifiedNodeState state) { + NodeState base = state.getBaseState(); + if (base instanceof ModifiedNodeState) { + ModifiedNodeState mbase = collapse((ModifiedNodeState) base); + + Map properties = + Maps.newHashMap(mbase.properties); + properties.putAll(state.properties); + + Map nodes = + Maps.newHashMap(mbase.nodes); + nodes.putAll(state.nodes); + + return new ModifiedNodeState( + mbase.getBaseState(), properties, nodes); + } else { + return state; + } + } + + /** + * The base state. + */ + private final NodeState base; + + /** + * Set of added, modified or removed ({@code null} value) + * property states. + */ + private final Map properties; + + /** + * Set of added, modified or removed ({@code null} value) + * child nodes. + */ + private final Map nodes; + + public ModifiedNodeState( + @Nonnull NodeState base, + @Nonnull Map properties, + @Nonnull Map nodes) { + this.base = checkNotNull(base); + this.properties = checkNotNull(properties); + this.nodes = checkNotNull(nodes); + } + + public ModifiedNodeState( + @Nonnull NodeState base, + @Nonnull Map properties) { + this(base, properties, ImmutableMap.of()); + } + + @Nonnull + public NodeState getBaseState() { + return base; + } + + //---------------------------------------------------------< NodeState >-- + + @Override + public NodeBuilder builder() { + return new MemoryNodeBuilder(this); + } + + @Override + public long getPropertyCount() { + long count = base.getPropertyCount(); + + for (Map.Entry entry : properties.entrySet()) { + if (base.getProperty(entry.getKey()) != null) { + count--; + } + if (entry.getValue() != null) { + count++; + } + } + + return count; + } + + @Override + public PropertyState getProperty(String name) { + PropertyState property = properties.get(name); + if (property != null) { + return property; + } else if (properties.containsKey(name)) { + return null; // removed + } else { + return base.getProperty(name); + } + } + + @Override + public Iterable getProperties() { + if (properties.isEmpty()) { + return base.getProperties(); // shortcut + } else { + Predicate filter = new Predicate() { + @Override + public boolean apply(PropertyState input) { + return !properties.containsKey(input.getName()); + } + }; + return concat( + filter(base.getProperties(), filter), + filter(properties.values(), notNull())); + } + } + + @Override + public long getChildNodeCount() { + long count = base.getChildNodeCount(); + + for (Map.Entry entry : nodes.entrySet()) { + if (base.getChildNode(entry.getKey()) != null) { + count--; + } + if (entry.getValue() != null) { + count++; + } + } + + return count; + } + + @Override + public boolean hasChildNode(String name) { + NodeState child = nodes.get(name); + if (child != null) { + return true; + } else if (nodes.containsKey(name)) { + return false; // removed + } else { + return base.hasChildNode(name); + } + } + + @Override + public NodeState getChildNode(String name) { + NodeState child = nodes.get(name); + if (child != null) { + return child; + } else if (nodes.containsKey(name)) { + return null; // removed + } else { + return base.getChildNode(name); + } + } + + @Override + public Iterable getChildNodeNames() { + if (nodes.isEmpty()) { + return base.getChildNodeNames(); // shortcut + } else { + return concat( + filter(base.getChildNodeNames(), not(in(nodes.keySet()))), + filterValues(nodes, notNull()).keySet()); + } + } + + @Override + public Iterable getChildNodeEntries() { + if (nodes.isEmpty()) { + return base.getChildNodeEntries(); // shortcut + } else { + Predicate filter = new Predicate() { + @Override + public boolean apply(ChildNodeEntry input) { + return !nodes.containsKey(input.getName()); + } + }; + return concat( + filter(base.getChildNodeEntries(), filter), + iterable(filterValues(nodes, notNull()).entrySet())); + } + } + + /** + * Since we keep track of an explicit base node state for a + * {@link ModifiedNodeState} instance, we can do this in two steps: + * first compare the base states to each other (often a fast operation), + * ignoring all changed properties and child nodes for which we have + * further modifications, and then compare all the modified properties + * and child nodes to those in the given base state. + */ + @Override + public void compareAgainstBaseState( + NodeState base, final NodeStateDiff diff) { + if (this.base != base) { + this.base.compareAgainstBaseState(base, new NodeStateDiff() { + @Override + public void propertyAdded(PropertyState after) { + if (!properties.containsKey(after.getName())) { + diff.propertyAdded(after); + } + } + @Override + public void propertyChanged( + PropertyState before, PropertyState after) { + if (!properties.containsKey(before.getName())) { + diff.propertyChanged(before, after); + } + } + @Override + public void propertyDeleted(PropertyState before) { + if (!properties.containsKey(before.getName())) { + diff.propertyDeleted(before); + } + } + @Override + public void childNodeAdded(String name, NodeState after) { + if (!nodes.containsKey(name)) { + diff.childNodeAdded(name, after); + } + } + @Override + public void childNodeChanged(String name, NodeState before, NodeState after) { + if (!nodes.containsKey(name)) { + diff.childNodeChanged(name, before, after); + } + } + @Override + public void childNodeDeleted(String name, NodeState before) { + if (!nodes.containsKey(name)) { + diff.childNodeDeleted(name, before); + } + } + }); + } + + for (Map.Entry entry : properties.entrySet()) { + PropertyState before = base.getProperty(entry.getKey()); + PropertyState after = entry.getValue(); + if (before == null && after == null) { + // do nothing + } else if (after == null) { + diff.propertyDeleted(before); + } else if (before == null) { + diff.propertyAdded(after); + } else if (!before.equals(after)) { + diff.propertyChanged(before, after); + } + } + + for (Map.Entry entry : nodes.entrySet()) { + String name = entry.getKey(); + NodeState before = base.getChildNode(name); + NodeState after = entry.getValue(); + if (before == null && after == null) { + // do nothing + } else if (after == null) { + diff.childNodeDeleted(name, before); + } else if (before == null) { + diff.childNodeAdded(name, after); + } else if (!before.equals(after)) { + diff.childNodeChanged(name, before, after); + } + } + } + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiBinaryPropertyState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiBinaryPropertyState.java new file mode 100644 index 00000000000..9342e1c59be --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiBinaryPropertyState.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.plugins.memory; + +import java.util.List; + +import com.google.common.collect.Lists; +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.value.Conversions; +import org.apache.jackrabbit.oak.plugins.value.Conversions.Converter; + +import static org.apache.jackrabbit.oak.api.Type.BINARIES; + +public class MultiBinaryPropertyState extends MultiPropertyState { + public MultiBinaryPropertyState(String name, Iterable values) { + super(name, values); + } + + /** + * Create a multi valued {@code PropertyState} from a list of {@link Blob}. + * @param name The name of the property state + * @param values The values of the property state + * @return The new property state of type {@link Type#BINARIES} + */ + public static PropertyState binaryPropertyFromBlob(String name, Iterable values) { + return new MultiBinaryPropertyState(name, values); + } + + /** + * Create a multi valued {@code PropertyState} from a list of byte arrays. + * @param name The name of the property state + * @param values The values of the property state + * @return The new property state of type {@link Type#BINARIES} + */ + public static PropertyState binaryPropertyFromArray(String name, Iterable values) { + List blobs = Lists.newArrayList(); + for (byte[] data : values) { + blobs.add(new ArrayBasedBlob(data)); + } + return new MultiBinaryPropertyState(name, blobs); + } + + @Override + public Converter getConverter(Blob value) { + return Conversions.convert(value); + } + + @Override + public Type getType() { + return BINARIES; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiBooleanPropertyState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiBooleanPropertyState.java new file mode 100644 index 00000000000..ecaa1211cf8 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiBooleanPropertyState.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.value.Conversions; +import org.apache.jackrabbit.oak.plugins.value.Conversions.Converter; + +import static org.apache.jackrabbit.oak.api.Type.BOOLEANS; + +public class MultiBooleanPropertyState extends MultiPropertyState { + public MultiBooleanPropertyState(String name, Iterable values) { + super(name, values); + } + + /** + * Create a multi valued {@code PropertyState} from a list of booleans. + * @param name The name of the property state + * @param values The values of the property state + * @return The new property state of type {@link Type#BOOLEANS} + */ + public static PropertyState booleanProperty(String name, Iterable values) { + return new MultiBooleanPropertyState(name, values); + } + + @Override + public Converter getConverter(Boolean value) { + return Conversions.convert(value); + } + + @Override + public Type getType() { + return BOOLEANS; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiDecimalPropertyState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiDecimalPropertyState.java new file mode 100644 index 00000000000..cb1da1b4c6d --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiDecimalPropertyState.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.plugins.memory; + +import java.math.BigDecimal; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.value.Conversions; +import org.apache.jackrabbit.oak.plugins.value.Conversions.Converter; + +import static org.apache.jackrabbit.oak.api.Type.DECIMALS; + +public class MultiDecimalPropertyState extends MultiPropertyState { + public MultiDecimalPropertyState(String name, Iterable values) { + super(name, values); + } + + /** + * Create a multi valued {@code PropertyState} from a list of decimals. + * @param name The name of the property state + * @param values The values of the property state + * @return The new property state of type {@link Type#DECIMALS} + */ + public static PropertyState decimalProperty(String name, Iterable values) { + return new MultiDecimalPropertyState(name, values); + } + + @Override + public Converter getConverter(BigDecimal value) { + return Conversions.convert(value); + } + + @Override + public Type getType() { + return DECIMALS; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiDoublePropertyState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiDoublePropertyState.java new file mode 100644 index 00000000000..8c6e4c29e9b --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiDoublePropertyState.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.plugins.memory; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.value.Conversions; +import org.apache.jackrabbit.oak.plugins.value.Conversions.Converter; + +import static org.apache.jackrabbit.oak.api.Type.DOUBLES; + +public class MultiDoublePropertyState extends MultiPropertyState { + public MultiDoublePropertyState(String name, Iterable values) { + super(name, values); + } + + /** + * Create a multi valued {@code PropertyState} from a list of doubles. + * @param name The name of the property state + * @param values The values of the property state + * @return The new property state of type {@link Type#DOUBLES} + */ + public static PropertyState doubleProperty(String name, Iterable values) { + return new MultiDoublePropertyState(name, values); + } + + @Override + public Converter getConverter(Double value) { + return Conversions.convert(value); + } + + @Override + public Type getType() { + return DOUBLES; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiGenericPropertyState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiGenericPropertyState.java new file mode 100644 index 00000000000..fae45f15f56 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiGenericPropertyState.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.value.Conversions; +import org.apache.jackrabbit.oak.plugins.value.Conversions.Converter; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.apache.jackrabbit.oak.api.Type.NAMES; +import static org.apache.jackrabbit.oak.api.Type.PATHS; +import static org.apache.jackrabbit.oak.api.Type.REFERENCES; +import static org.apache.jackrabbit.oak.api.Type.URIS; +import static org.apache.jackrabbit.oak.api.Type.WEAKREFERENCES; + +public class MultiGenericPropertyState extends MultiPropertyState { + private final Type type; + + /** + * @throws IllegalArgumentException if {@code type.isArray()} is {@code false} + */ + public MultiGenericPropertyState(String name, Iterable values, Type type) { + super(name, values); + checkArgument(type.isArray()); + this.type = type; + } + + /** + * Create a multi valued {@code PropertyState} from a list of names. + * No validation is performed on the strings passed for {@code values}. + * @param name The name of the property state + * @param values The values of the property state + * @return The new property state of type {@link Type#NAMES} + */ + public static PropertyState nameProperty(String name, Iterable values) { + return new MultiGenericPropertyState(name, values, NAMES); + } + + /** + * Create a multi valued {@code PropertyState} from a list of paths. + * No validation is performed on the strings passed for {@code values}. + * @param name The name of the property state + * @param values The values of the property state + * @return The new property state of type {@link Type#PATHS} + */ + public static PropertyState pathProperty(String name, Iterable values) { + return new MultiGenericPropertyState(name, values, PATHS); + } + + /** + * Create a multi valued {@code PropertyState} from a list of references. + * No validation is performed on the strings passed for {@code values}. + * @param name The name of the property state + * @param values The values of the property state + * @return The new property state of type {@link Type#REFERENCES} + */ + public static PropertyState referenceProperty(String name, Iterable values) { + return new MultiGenericPropertyState(name, values, REFERENCES); + } + + /** + * Create a multi valued {@code PropertyState} from a list of weak references. + * No validation is performed on the strings passed for {@code values}. + * @param name The name of the property state + * @param values The values of the property state + * @return The new property state of type {@link Type#WEAKREFERENCES} + */ + public static PropertyState weakreferenceProperty(String name, Iterable values) { + return new MultiGenericPropertyState(name, values, WEAKREFERENCES); + } + + /** + * Create a multi valued {@code PropertyState} from a list of URIs. + * No validation is performed on the strings passed for {@code values}. + * @param name The name of the property state + * @param values The values of the property state + * @return The new property state of type {@link Type#URIS} + */ + public static PropertyState uriProperty(String name, Iterable values) { + return new MultiGenericPropertyState(name, values, URIS); + } + + @Override + public Converter getConverter(String value) { + return Conversions.convert(value); + } + + @Override + public Type getType() { + return type; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiLongPropertyState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiLongPropertyState.java new file mode 100644 index 00000000000..417cab97a3f --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiLongPropertyState.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import java.util.Calendar; +import java.util.List; + +import com.google.common.collect.Lists; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.value.Conversions; +import org.apache.jackrabbit.oak.plugins.value.Conversions.Converter; + +public class MultiLongPropertyState extends MultiPropertyState { + private final Type type; + + public MultiLongPropertyState(String name, Iterable values, Type type) { + super(name, values); + this.type = type; + } + + /** + * Create a multi valued {@code PropertyState} from a list of longs. + * @param name The name of the property state + * @param values The values of the property state + * @return The new property state of type {@link Type#LONGS} + */ + public static PropertyState createLongProperty(String name, Iterable values) { + return new MultiLongPropertyState(name, Lists.newArrayList(values), Type.LONGS); + } + + /** + * Create a multi valued {@code PropertyState} of dates from a list of longs. + * @param name The name of the property state + * @param values The values of the property state + * @return The new property state of type {@link Type#DATES} + */ + public static PropertyState createDatePropertyFromLong(String name, Iterable values) { + return new MultiLongPropertyState(name, Lists.newArrayList(values), Type.DATES); + } + + /** + * Create a multi valued {@code PropertyState} of dates. + * @param name The name of the property state + * @param values The values of the property state + * @return The new property state of type {@link Type#DATES} + */ + public static PropertyState createDatePropertyFromCalendar(String name, Iterable values) { + List dates = Lists.newArrayList(); + for (Calendar v : values) { + dates.add(Conversions.convert(v).toLong()); + } + return new MultiLongPropertyState(name, dates, Type.DATES); + } + + /** + * Create a multi valued {@code PropertyState} of dates from a list of strings. + * @param name The name of the property state + * @param values The values of the property state + * @return The new property state of type {@link Type#DATES} + * @throws IllegalArgumentException if one of the {@code values} is not a parseable to a date. + */ + public static PropertyState createDateProperty(String name, Iterable values) { + List dates = Lists.newArrayList(); + for (String v : values) { + dates.add(Conversions.convert(Conversions.convert(v).toCalendar()).toLong()); + } + return new MultiLongPropertyState(name, dates, Type.DATES); + } + + @Override + public Converter getConverter(Long value) { + if (type == Type.DATES) { + return Conversions.convert(Conversions.convert(value).toCalendar()); + } + else { + return Conversions.convert(value); + } + } + + @Override + public Type getType() { + return type; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiPropertyState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiPropertyState.java new file mode 100644 index 00000000000..f4e01e352e5 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiPropertyState.java @@ -0,0 +1,215 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import java.math.BigDecimal; +import java.util.List; + +import javax.annotation.Nonnull; +import javax.jcr.PropertyType; + +import com.google.common.base.Function; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.value.Conversions.Converter; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Abstract base class for multi valued {@code PropertyState} implementations. + */ +abstract class MultiPropertyState extends EmptyPropertyState { + protected final List values; + + /** + * Create a new property state with the given {@code name} + * and {@code values} + * @param name The name of the property state. + * @param values The values of the property state. + */ + protected MultiPropertyState(String name, Iterable values) { + super(name); + this.values = Lists.newArrayList(values); + } + + /** + * Create a converter for converting a value to other types. + * @param value The value to convert + * @return A converter for the value of this property + */ + public abstract Converter getConverter(T value); + + @SuppressWarnings("unchecked") + private S convertTo(Type type) { + switch (type.tag()) { + case PropertyType.STRING: + return (S) Iterables.transform(values, new Function() { + @Override + public String apply(T value) { + return getConverter(value).toString(); + } + }); + case PropertyType.BINARY: + return (S) Iterables.transform(values, new Function() { + @Override + public Blob apply(T value) { + return getConverter(value).toBinary(); + } + }); + case PropertyType.LONG: + return (S) Iterables.transform(values, new Function() { + @Override + public Long apply(T value) { + return getConverter(value).toLong(); + } + }); + case PropertyType.DOUBLE: + return (S) Iterables.transform(values, new Function() { + @Override + public Double apply(T value) { + return getConverter(value).toDouble(); + } + }); + case PropertyType.DATE: + return (S) Iterables.transform(values, new Function() { + @Override + public String apply(T value) { + return getConverter(value).toDate(); + } + }); + case PropertyType.BOOLEAN: + return (S) Iterables.transform(values, new Function() { + @Override + public Boolean apply(T value) { + return getConverter(value).toBoolean(); + } + }); + case PropertyType.NAME: + return (S) Iterables.transform(values, new Function() { + @Override + public String apply(T value) { + return getConverter(value).toString(); + } + }); + case PropertyType.PATH: + return (S) Iterables.transform(values, new Function() { + @Override + public String apply(T value) { + return getConverter(value).toString(); + } + }); + case PropertyType.REFERENCE: + return (S) Iterables.transform(values, new Function() { + @Override + public String apply(T value) { + return getConverter(value).toString(); + } + }); + case PropertyType.WEAKREFERENCE: + return (S) Iterables.transform(values, new Function() { + @Override + public String apply(T value) { + return getConverter(value).toString(); + } + }); + case PropertyType.URI: + return (S) Iterables.transform(values, new Function() { + @Override + public String apply(T value) { + return getConverter(value).toString(); + } + }); + case PropertyType.DECIMAL: + return (S) Iterables.transform(values, new Function() { + @Override + public BigDecimal apply(T value) { + return getConverter(value).toDecimal(); + } + }); + default: throw new IllegalArgumentException("Unknown type:" + type); + } + } + + /** + * @throws IllegalArgumentException if {@code type} is not one of the + * values defined in {@link Type} or if {@code type.isArray()} is {@code false}. + */ + @SuppressWarnings("unchecked") + @Nonnull + @Override + public S getValue(Type type) { + checkArgument(type.isArray(), "Type must not be an array type"); + if (getType() == type) { + return (S) values; + } + else { + return convertTo(type); + } + } + + @SuppressWarnings("unchecked") + private S convertTo(Type type, int index) { + switch (type.tag()) { + case PropertyType.STRING: return (S) getConverter(values.get(index)).toString(); + case PropertyType.BINARY: return (S) getConverter(values.get(index)).toBinary(); + case PropertyType.LONG: return (S) (Long) getConverter(values.get(index)).toLong(); + case PropertyType.DOUBLE: return (S) (Double) getConverter(values.get(index)).toDouble(); + case PropertyType.DATE: return (S) getConverter(values.get(index)).toDate(); + case PropertyType.BOOLEAN: return (S) (Boolean) getConverter(values.get(index)).toBoolean(); + case PropertyType.NAME: return (S) getConverter(values.get(index)).toString(); + case PropertyType.PATH: return (S) getConverter(values.get(index)).toString(); + case PropertyType.REFERENCE: return (S) getConverter(values.get(index)).toString(); + case PropertyType.WEAKREFERENCE: return (S) getConverter(values.get(index)).toString(); + case PropertyType.URI: return (S) getConverter(values.get(index)).toString(); + case PropertyType.DECIMAL: return (S) getConverter(values.get(index)).toDecimal(); + default: throw new IllegalArgumentException("Unknown type:" + type); + } + } + + /** + * @throws IllegalArgumentException if {@code type} is not one of the + * values defined in {@link Type} or if {@code type.isArray()} is {@code true} + * @throws IndexOutOfBoundsException if {@code index >= count()}. + */ + @SuppressWarnings("unchecked") + @Nonnull + @Override + public S getValue(Type type, int index) { + checkArgument(!type.isArray(), "Type must not be an array type"); + if (getType().getBaseType() == type) { + return (S) values.get(index); + } + else { + return convertTo(type, index); + } + } + + @Override + public final int count() { + return values.size(); + } + + @Override + public long size(int index) { + return convertTo(Type.STRING, index).length(); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiStringPropertyState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiStringPropertyState.java new file mode 100644 index 00000000000..902e4b248b1 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MultiStringPropertyState.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.value.Conversions; +import org.apache.jackrabbit.oak.plugins.value.Conversions.Converter; + +public class MultiStringPropertyState extends MultiPropertyState { + public MultiStringPropertyState(String name, Iterable values) { + super(name, values); + } + + /** + * Create a multi valued {@code PropertyState} from a list of strings. + * @param name The name of the property state + * @param values The values of the property state + * @return The new property state of type {@link Type#STRINGS} + */ + public static PropertyState stringProperty(String name, Iterable values) { + return new MultiStringPropertyState(name, values); + } + + @Override + public Converter getConverter(String value) { + return Conversions.convert(value); + } + + @Override + public Type getType() { + return Type.STRINGS; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/PropertyStates.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/PropertyStates.java new file mode 100644 index 00000000000..8e55fa7b3d2 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/PropertyStates.java @@ -0,0 +1,361 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import java.math.BigDecimal; +import java.util.List; + +import javax.annotation.Nonnull; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.json.JsopReader; +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.kernel.KernelBlob; +import org.apache.jackrabbit.oak.kernel.TypeCodes; +import org.apache.jackrabbit.oak.plugins.value.Conversions; + +import static org.apache.jackrabbit.oak.api.Type.STRINGS; + +/** + * Utility class for creating {@link PropertyState} instances. + */ +public final class PropertyStates { + private PropertyStates() {} + + /** + * Create a {@code PropertyState} based on a {@link Value}. The + * {@link Type} of the property state is determined by the + * type of the value. + * @param name The name of the property state + * @param value The value of the property state + * @return The new property state + * @throws RepositoryException forwarded from {@code value} + */ + @Nonnull + public static PropertyState createProperty(String name, Value value) throws RepositoryException { + int type = value.getType(); + switch (type) { + case PropertyType.STRING: + return StringPropertyState.stringProperty(name, value.getString()); + case PropertyType.BINARY: + return BinaryPropertyState.binaryProperty(name, value); + case PropertyType.LONG: + return LongPropertyState.createLongProperty(name, value.getLong()); + case PropertyType.DOUBLE: + return DoublePropertyState.doubleProperty(name, value.getDouble()); + case PropertyType.DATE: + return LongPropertyState.createDateProperty(name, value.getLong()); + case PropertyType.BOOLEAN: + return BooleanPropertyState.booleanProperty(name, value.getBoolean()); + case PropertyType.DECIMAL: + return DecimalPropertyState.decimalProperty(name, value.getDecimal()); + default: + return new GenericPropertyState(name, value.getString(), Type.fromTag(type, false)); + } + } + + /** + * Create a multi valued {@code PropertyState} based on a list of + * {@link Value} instances. The {@link Type} of the property is determined + * by the type of the first value in the list or {@link Type#STRING} if the + * list is empty. + * + * @param name The name of the property state + * @param values The values of the property state + * @return The new property state + * @throws RepositoryException forwarded from {@code value} + */ + @Nonnull + public static PropertyState createProperty(String name, Iterable values) throws RepositoryException { + Value first = Iterables.getFirst(values, null); + if (first == null) { + return EmptyPropertyState.emptyProperty(name, STRINGS); + } + + int type = first.getType(); + switch (type) { + case PropertyType.STRING: + List strings = Lists.newArrayList(); + for (Value value : values) { + strings.add(value.getString()); + } + return MultiStringPropertyState.stringProperty(name, strings); + case PropertyType.BINARY: + List blobs = Lists.newArrayList(); + for (Value value : values) { + blobs.add(new ValueBasedBlob(value)); + } + return MultiBinaryPropertyState.binaryPropertyFromBlob(name, blobs); + case PropertyType.LONG: + List longs = Lists.newArrayList(); + for (Value value : values) { + longs.add(value.getLong()); + } + return MultiLongPropertyState.createLongProperty(name, longs); + case PropertyType.DOUBLE: + List doubles = Lists.newArrayList(); + for (Value value : values) { + doubles.add(value.getDouble()); + } + return MultiDoublePropertyState.doubleProperty(name, doubles); + case PropertyType.DATE: + List dates = Lists.newArrayList(); + for (Value value : values) { + dates.add(value.getLong()); + } + return MultiLongPropertyState.createDatePropertyFromLong(name, dates); + case PropertyType.BOOLEAN: + List booleans = Lists.newArrayList(); + for (Value value : values) { + booleans.add(value.getBoolean()); + } + return MultiBooleanPropertyState.booleanProperty(name, booleans); + case PropertyType.DECIMAL: + List decimals = Lists.newArrayList(); + for (Value value : values) { + decimals.add(value.getDecimal()); + } + return MultiDecimalPropertyState.decimalProperty(name, decimals); + default: + List vals = Lists.newArrayList(); + for (Value value : values) { + vals.add(value.getString()); + } + return new MultiGenericPropertyState(name, vals, Type.fromTag(type, true)); + } + } + + /** + * Create a {@code PropertyState} from a string. + * @param name The name of the property state + * @param value The value of the property state + * @param type The type of the property state + * @return The new property state + */ + @Nonnull + public static PropertyState createProperty(String name, String value, int type) { + switch (type) { + case PropertyType.STRING: + return StringPropertyState.stringProperty(name, value); + case PropertyType.BINARY: + return BinaryPropertyState.binaryProperty(name, Conversions.convert(value).toBinary()); + case PropertyType.LONG: + return LongPropertyState.createLongProperty(name, Conversions.convert(value).toLong()); + case PropertyType.DOUBLE: + return DoublePropertyState.doubleProperty(name, Conversions.convert(value).toDouble()); + case PropertyType.DATE: + return LongPropertyState.createDateProperty(name, value); + case PropertyType.BOOLEAN: + return BooleanPropertyState.booleanProperty(name, Conversions.convert(value).toBoolean()); + case PropertyType.DECIMAL: + return DecimalPropertyState.decimalProperty(name, Conversions.convert(value).toDecimal()); + default: + return new GenericPropertyState(name, value, Type.fromTag(type, false)); + } + } + + /** + * Create a {@code PropertyState}. + * @param name The name of the property state + * @param value The value of the property state + * @param type The type of the property state + * @return The new property state + */ + @SuppressWarnings("unchecked") + @Nonnull + public static PropertyState createProperty(String name, T value, Type type) { + switch (type.tag()) { + case PropertyType.STRING: + return type.isArray() + ? MultiStringPropertyState.stringProperty(name, (Iterable) value) + : StringPropertyState.stringProperty(name, (String) value); + case PropertyType.BINARY: + return type.isArray() + ? MultiBinaryPropertyState.binaryPropertyFromBlob(name, (Iterable) value) + : BinaryPropertyState.binaryProperty(name, (Blob) value); + case PropertyType.LONG: + return type.isArray() + ? MultiLongPropertyState.createLongProperty(name, (Iterable) value) + : LongPropertyState.createLongProperty(name, (Long) value); + case PropertyType.DOUBLE: + return type.isArray() + ? MultiDoublePropertyState.doubleProperty(name, (Iterable) value) + : DoublePropertyState.doubleProperty(name, (Double) value); + case PropertyType.DATE: + return type.isArray() + ? MultiLongPropertyState.createDateProperty(name, (Iterable) value) + : LongPropertyState.createDateProperty(name, (String) value); + case PropertyType.BOOLEAN: + return type.isArray() + ? MultiBooleanPropertyState.booleanProperty(name, (Iterable) value) + : BooleanPropertyState.booleanProperty(name, (Boolean) value); + case PropertyType.NAME: + return type.isArray() + ? MultiGenericPropertyState.nameProperty(name, (Iterable) value) + : GenericPropertyState.nameProperty(name, (String) value); + case PropertyType.PATH: + return type.isArray() + ? MultiGenericPropertyState.pathProperty(name, (Iterable) value) + : GenericPropertyState.pathProperty(name, (String) value); + case PropertyType.REFERENCE: + return type.isArray() + ? MultiGenericPropertyState.referenceProperty(name, (Iterable) value) + : GenericPropertyState.referenceProperty(name, (String) value); + case PropertyType.WEAKREFERENCE: + return type.isArray() + ? MultiGenericPropertyState.weakreferenceProperty(name, (Iterable) value) + : GenericPropertyState.weakreferenceProperty(name, (String) value); + case PropertyType.URI: + return type.isArray() + ? MultiGenericPropertyState.uriProperty(name, (Iterable) value) + : GenericPropertyState.uriProperty(name, (String) value); + case PropertyType.DECIMAL: + return type.isArray() + ? MultiDecimalPropertyState.decimalProperty(name, (Iterable) value) + : DecimalPropertyState.decimalProperty(name, (BigDecimal) value); + default: throw new IllegalArgumentException("Invalid type: " + type); + } + } + + /** + * Create a {@code PropertyState} where the {@link Type} of the property state + * is inferred from the runtime type of {@code T} according to the mapping + * established through {@code Type}. + * @param name The name of the property state + * @param value The value of the property state + * @return The new property state + */ + @Nonnull + public static PropertyState createProperty(String name, T value) { + if (value instanceof String) { + return StringPropertyState.stringProperty(name, (String) value); + } + else if (value instanceof Blob) { + return BinaryPropertyState.binaryProperty(name, (Blob) value); + } + else if (value instanceof byte[]) { + return BinaryPropertyState.binaryProperty(name, (byte[]) value); + } + else if (value instanceof Long) { + return LongPropertyState.createLongProperty(name, (Long) value); + } + else if (value instanceof Integer) { + return LongPropertyState.createLongProperty(name, (long) (Integer) value); + } + else if (value instanceof Double) { + return DoublePropertyState.doubleProperty(name, (Double) value); + } + else if (value instanceof Boolean) { + return BooleanPropertyState.booleanProperty(name, (Boolean) value); + } + else if (value instanceof BigDecimal) { + return DecimalPropertyState.decimalProperty(name, (BigDecimal) value); + } + else { + throw new IllegalArgumentException("Can't infer type of value of class '" + value.getClass() + '\''); + } + } + + /** + * Read a {@code PropertyState} from a {@link JsopReader} + * @param name The name of the property state + * @param reader The reader + * @param kernel {@link MicroKernel} instance used to resolve binaries + * @return The new property state of type {@link Type#DECIMALS} + */ + public static PropertyState readProperty(String name, JsopReader reader, MicroKernel kernel) { + if (reader.matches(JsopReader.NUMBER)) { + String number = reader.getToken(); + return createProperty(name, number, PropertyType.LONG); + } else if (reader.matches(JsopReader.TRUE)) { + return BooleanPropertyState.booleanProperty(name, true); + } else if (reader.matches(JsopReader.FALSE)) { + return BooleanPropertyState.booleanProperty(name, false); + } else if (reader.matches(JsopReader.STRING)) { + String jsonString = reader.getToken(); + if (TypeCodes.startsWithCode(jsonString)) { + int type = TypeCodes.getTypeForCode(jsonString.substring(0, 3)); + String value = jsonString.substring(4); + if (type == PropertyType.BINARY) { + return BinaryPropertyState.binaryProperty(name, new KernelBlob(value, kernel)); + } else { + return createProperty(name, value, type); + } + } else { + return StringPropertyState.stringProperty(name, jsonString); + } + } else { + throw new IllegalArgumentException("Unexpected token: " + reader.getToken()); + } + } + + /** + * Read a multi valued {@code PropertyState} from a {@link JsopReader} + * @param name The name of the property state + * @param reader The reader + * @param kernel {@link MicroKernel} instance used to resolve binaries + * @return The new property state of type {@link Type#DECIMALS} + */ + public static PropertyState readArrayProperty(String name, JsopReader reader, MicroKernel kernel) { + int type = PropertyType.STRING; + List values = Lists.newArrayList(); + while (!reader.matches(']')) { + if (reader.matches(JsopReader.NUMBER)) { + String number = reader.getToken(); + type = PropertyType.LONG; + values.add(Conversions.convert(number).toLong()); + } else if (reader.matches(JsopReader.TRUE)) { + type = PropertyType.BOOLEAN; + values.add(true); + } else if (reader.matches(JsopReader.FALSE)) { + type = PropertyType.BOOLEAN; + values.add(false); + } else if (reader.matches(JsopReader.STRING)) { + String jsonString = reader.getToken(); + if (TypeCodes.startsWithCode(jsonString)) { + type = TypeCodes.getTypeForCode(jsonString.substring(0, 3)); + String value = jsonString.substring(4); + if (type == PropertyType.BINARY) { + values.add(new KernelBlob(value, kernel)); + } else if(type == PropertyType.DOUBLE) { + values.add(Conversions.convert(value).toDouble()); + } else if(type == PropertyType.DECIMAL) { + values.add(Conversions.convert(value).toDecimal()); + } else { + values.add(value); + } + } else { + type = PropertyType.STRING; + values.add(jsonString); + } + } else { + throw new IllegalArgumentException("Unexpected token: " + reader.getToken()); + } + reader.matches(','); + } + return createProperty(name, values, (Type) Type.fromTag(type, true)); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/SinglePropertyState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/SinglePropertyState.java new file mode 100644 index 00000000000..38f70e69777 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/SinglePropertyState.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import javax.annotation.Nonnull; +import javax.jcr.PropertyType; + +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.value.Conversions.Converter; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Collections.singleton; + +/** + * Abstract base class for single valued {@code PropertyState} implementations. + */ +abstract class SinglePropertyState extends EmptyPropertyState { + + /** + * Create a new property state with the given {@code name} + * @param name The name of the property state. + */ + protected SinglePropertyState(String name) { + super(name); + } + + /** + * @return {@code false} + */ + @Override + public boolean isArray() { + return false; + } + + /** + * Create a converter for converting the value of this property to other types. + * @return A converter for the value of this property + */ + public abstract Converter getConverter(); + + @SuppressWarnings("unchecked") + private S convertTo(Type type) { + switch (type.tag()) { + case PropertyType.STRING: return (S) getConverter().toString(); + case PropertyType.BINARY: return (S) getConverter().toBinary(); + case PropertyType.LONG: return (S) (Long) getConverter().toLong(); + case PropertyType.DOUBLE: return (S) (Double) getConverter().toDouble(); + case PropertyType.DATE: return (S) getConverter().toDate(); + case PropertyType.BOOLEAN: return (S) (Boolean) getConverter().toBoolean(); + case PropertyType.NAME: return (S) getConverter().toString(); + case PropertyType.PATH: return (S) getConverter().toString(); + case PropertyType.REFERENCE: return (S) getConverter().toString(); + case PropertyType.WEAKREFERENCE: return (S) getConverter().toString(); + case PropertyType.URI: return (S) getConverter().toString(); + case PropertyType.DECIMAL: return (S) getConverter().toDecimal(); + default: throw new IllegalArgumentException("Unknown type:" + type); + } + } + + /** + * The value of this property + * @return Value of this property + */ + public abstract T getValue(); + + /** + * @throws IllegalArgumentException if {@code type} is not one of the + * values defined in {@link Type}. + */ + @SuppressWarnings("unchecked") + @Nonnull + @Override + public S getValue(Type type) { + if (type.isArray()) { + if (getType() == type.getBaseType()) { + return (S) singleton(getValue()); + } + else { + return (S) singleton(convertTo(type.getBaseType())); + } + } + else { + if (getType() == type) { + return (S) getValue(); + } + else { + return convertTo(type); + } + } + } + + /** + * @throws IllegalArgumentException if {@code type.isArray} is {@code true} + * @throws IndexOutOfBoundsException if {@code index != 0} + */ + @Nonnull + @Override + public S getValue(Type type, int index) { + checkArgument(!type.isArray(), "Type must not be an array type"); + if (index != 0) { + throw new IndexOutOfBoundsException(String.valueOf(index)); + } + return getValue(type); + } + + /** + * @return {@code getString().length()} + */ + @Override + public long size() { + return convertTo(Type.STRING).length(); + } + + /** + * @return {@code size} + * @throws IndexOutOfBoundsException if {@code index != 0} + */ + @Override + public long size(int index) { + if (index != 0) { + throw new IndexOutOfBoundsException(String.valueOf(index)); + } + return size(); + } + + /** + * @return {@code 1} + */ + @Override + public int count() { + return 1; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/StringBasedBlob.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/StringBasedBlob.java new file mode 100644 index 00000000000..3d49bd62a29 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/StringBasedBlob.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +import javax.annotation.Nonnull; + +import com.google.common.base.Charsets; + +/** + * This {@code Blob} implementations is based on a string. + */ +public class StringBasedBlob extends AbstractBlob { + private final String value; + + public StringBasedBlob(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + + /** + * This implementation returns the bytes of the UTF-8 encoding + * of the underlying string. + */ + @Nonnull + @Override + public InputStream getNewStream() { + return new ByteArrayInputStream(value.getBytes(Charsets.UTF_8)); + } + + /** + * This implementation returns the number of bytes in the UTF-8 encoding + * of the underlying string. + */ + @Override + public long length() { + return value.getBytes(Charsets.UTF_8).length; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/StringPropertyState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/StringPropertyState.java new file mode 100644 index 00000000000..308b400a300 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/StringPropertyState.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.value.Conversions; +import org.apache.jackrabbit.oak.plugins.value.Conversions.Converter; + +import static org.apache.jackrabbit.oak.api.Type.STRING; + +public class StringPropertyState extends SinglePropertyState { + private final String value; + + public StringPropertyState(String name, String value) { + super(name); + this.value = value; + } + + /** + * Create a {@code PropertyState} from a string. + * @param name The name of the property state + * @param value The value of the property state + * @return The new property state of type {@link Type#STRING} + */ + public static PropertyState stringProperty(String name, String value) { + return new StringPropertyState(name, value); + } + + @Override + public String getValue() { + return value; + } + + @Override + public Converter getConverter() { + return Conversions.convert(value); + } + + @Override + public Type getType() { + return STRING; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/ValueBasedBlob.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/ValueBasedBlob.java new file mode 100644 index 00000000000..92e5ff560de --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/ValueBasedBlob.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import java.io.IOException; +import java.io.InputStream; + +import javax.annotation.Nonnull; +import javax.jcr.Binary; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +/** + * This {@code Blob} implementations is based on {@link Value} + */ +public class ValueBasedBlob extends AbstractBlob { + private final Value value; + + public ValueBasedBlob(Value value) { + this.value = value; + } + + /** + * This implementation return the stream of the underlying {@code Value}. + */ + @Nonnull + @Override + public InputStream getNewStream() { + return new ValueBasedInputStream(value); + } + + /** + * This implementation returns the size of the {@link Binary} of the underlying + * {@code Value}. + */ + @Override + public long length() { + try { + Binary binary = value.getBinary(); + try { + return binary.getSize(); + } + finally { + binary.dispose(); + } + } + catch (RepositoryException e) { + return -1; + } + } + + private static class ValueBasedInputStream extends InputStream { + private final Value value; + + private Binary binary; + private InputStream stream; + + public ValueBasedInputStream(Value value) { + this.value = value; + } + + @Override + public int read() throws IOException { + return stream().read(); + } + + @Override + public int read(byte[] b) throws IOException { + return stream().read(b); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return stream().read(b, off, len); + } + + @Override + public long skip(long n) throws IOException { + return stream().skip(n); + } + + @Override + public int available() throws IOException { + return stream().available(); + } + + @Override + public void close() throws IOException { + if (stream != null) { + stream.close(); + stream = null; + } + if (binary != null) { + binary.dispose(); + binary = null; + } + } + + @Override + public synchronized void mark(int readlimit) { + } + + @Override + public synchronized void reset() throws IOException { + throw new IOException("mark not supported"); + } + + @Override + public boolean markSupported() { + return false; + } + + private InputStream stream() throws IOException { + try { + if (binary == null) { + binary = value.getBinary(); + stream = binary.getStream(); + } + return stream; + } + catch (RepositoryException e) { + throw new IOException(e); + } + } + + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NameValidator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NameValidator.java new file mode 100644 index 00000000000..68bb920ae48 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NameValidator.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.plugins.name; + +import java.util.Set; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import static org.apache.jackrabbit.oak.api.Type.NAME; +import static org.apache.jackrabbit.oak.api.Type.NAMES; + +class NameValidator implements Validator { + + private final Set prefixes; + + public NameValidator(Set prefixes) { + this.prefixes = prefixes; + } + + protected void checkValidName(String name) throws CommitFailedException { + int colon = name.indexOf(':'); + if (colon > 0) { + String prefix = name.substring(0, colon); + if (prefix.isEmpty() || !prefixes.contains(prefix)) { + throw new CommitFailedException( + "Invalid namespace prefix: " + name); + } + } + + String local = name.substring(colon + 1); + if (!Namespaces.isValidLocalName(local)) { + throw new CommitFailedException("Invalid name: " + name); + } + } + + protected void checkValidValue(PropertyState property) + throws CommitFailedException { + if (NAME.equals(property.getType())) { + for (String value : property.getValue(NAMES)) { + checkValidValue(value); + } + } + } + + protected void checkValidValue(String value) + throws CommitFailedException { + checkValidName(value); + } + + //-------------------------------------------------------< NodeValidator > + + @Override + public void propertyAdded(PropertyState after) + throws CommitFailedException { + checkValidName(after.getName()); + checkValidValue(after); + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) + throws CommitFailedException { + checkValidValue(after); + } + + @Override + public void propertyDeleted(PropertyState before) { + // do nothing + } + + @Override + public Validator childNodeAdded(String name, NodeState after) + throws CommitFailedException { + checkValidName(name); + return this; + } + + @Override + public Validator childNodeChanged( + String name, NodeState before, NodeState after) { + return this; + } + + @Override + public Validator childNodeDeleted(String name, NodeState before) { + return null; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NameValidatorProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NameValidatorProvider.java new file mode 100644 index 00000000000..e700218b678 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NameValidatorProvider.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.plugins.name; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.apache.jackrabbit.oak.core.ReadOnlyTree; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * Validator service that checks that all node and property names as well + * as any name values are syntactically valid and that any namespace prefixes + * are properly registered. + */ +@Component +@Service(ValidatorProvider.class) +public class NameValidatorProvider implements ValidatorProvider { + + @Override + public Validator getRootValidator(NodeState before, NodeState after) { + return new NameValidator( + Namespaces.getNamespaceMap(new ReadOnlyTree(after)).keySet()); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NamespaceConstants.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NamespaceConstants.java new file mode 100644 index 00000000000..6e279d2e342 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NamespaceConstants.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.name; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import javax.jcr.NamespaceRegistry; + +import org.apache.jackrabbit.JcrConstants; + +/** + * NamespaceConstants... TODO + */ +public interface NamespaceConstants { + + String REP_NAMESPACES = "rep:namespaces"; + + String NAMESPACES_PATH = '/' + JcrConstants.JCR_SYSTEM + '/' + REP_NAMESPACES; + + // TODO: see http://java.net/jira/browse/JSR_333-50) + String PREFIX_SV = "sv"; + String NAMESPACE_SV = "http://www.jcp.org/jcr/sv/1.0"; + + String PREFIX_REP = "rep"; + String NAMESPACE_REP = "internal"; // TODO: see OAK-74 + + // additional XML namespace + String PREFIX_XMLNS = "xmlns"; + String NAMESPACE_XMLNS = "http://www.w3.org/2000/xmlns/"; + + /** + * Reserved namespace prefixes as defined in jackrabbit 2 + */ + Collection RESERVED_PREFIXES = Collections.unmodifiableList(Arrays.asList( + NamespaceRegistry.PREFIX_XML, + NamespaceRegistry.PREFIX_JCR, + NamespaceRegistry.PREFIX_NT, + NamespaceRegistry.PREFIX_MIX, + PREFIX_XMLNS, + PREFIX_REP, + PREFIX_SV + )); + + /** + * Reserved namespace URIs as defined in jackrabbit 2 + */ + Collection RESERVED_URIS = Collections.unmodifiableList(Arrays.asList( + NamespaceRegistry.NAMESPACE_XML, + NamespaceRegistry.NAMESPACE_JCR, + NamespaceRegistry.NAMESPACE_NT, + NamespaceRegistry.NAMESPACE_MIX, + NAMESPACE_XMLNS, + NAMESPACE_REP, + NAMESPACE_SV + )); +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NamespaceValidator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NamespaceValidator.java new file mode 100644 index 00000000000..610bc37d03a --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NamespaceValidator.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.plugins.name; + +import java.util.Locale; +import java.util.Map; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.commit.DefaultValidator; + +import static org.apache.jackrabbit.oak.api.Type.STRING; + +class NamespaceValidator extends DefaultValidator { + + private final Map map; + + public NamespaceValidator(Map map) { + this.map = map; + } + + //----------------------------------------------------------< Validator >--- + @Override + public void propertyAdded(PropertyState after) + throws CommitFailedException { + String prefix = after.getName(); + // ignore jcr:primaryType + if (prefix.equals("jcr:primaryType")) { + return; + } + if (map.containsKey(prefix)) { + throw new NamespaceValidatorException( + "Namespace mapping already registered", prefix); + } else if (Namespaces.isValidPrefix(prefix)) { + if (after.isArray() || !STRING.equals(after.getType())) { + throw new NamespaceValidatorException( + "Invalid namespace mapping", prefix); + } else if (prefix.toLowerCase(Locale.ENGLISH).startsWith("xml")) { + throw new NamespaceValidatorException( + "XML prefixes are reserved", prefix); + } else if (map.containsValue(after.getValue(STRING))) { + throw modificationNotAllowed(prefix); + } + } else { + throw new NamespaceValidatorException( + "Not a valid namespace prefix", prefix); + } + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) + throws CommitFailedException { + if (map.containsKey(after.getName())) { + throw modificationNotAllowed(after.getName()); + } + } + + @Override + public void propertyDeleted(PropertyState before) + throws CommitFailedException { + if (map.containsKey(before.getName())) { + // TODO: Check whether this namespace is still used in content + } + } + + private static NamespaceValidatorException modificationNotAllowed(String prefix) { + return new NamespaceValidatorException( + "Namespace modification not allowed", prefix); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NamespaceValidatorException.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NamespaceValidatorException.java new file mode 100644 index 00000000000..da173170e9c --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NamespaceValidatorException.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.plugins.name; + +import javax.jcr.NamespaceException; + +import org.apache.jackrabbit.oak.api.CommitFailedException; + +class NamespaceValidatorException extends CommitFailedException { + + public NamespaceValidatorException(String message, String prefix) { + super(message + ": " + prefix); + } + + public NamespaceException getNamespaceException() { + return new NamespaceException(getMessage(), this); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NamespaceValidatorProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NamespaceValidatorProvider.java new file mode 100644 index 00000000000..3cd93e65669 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NamespaceValidatorProvider.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.plugins.name; + +import static org.apache.jackrabbit.JcrConstants.JCR_SYSTEM; +import static org.apache.jackrabbit.oak.plugins.name.NamespaceConstants.REP_NAMESPACES; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.apache.jackrabbit.oak.core.ReadOnlyTree; +import org.apache.jackrabbit.oak.spi.commit.SubtreeValidator; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * Validator service that checks that all node and property names as well + * as any name values are syntactically valid and that any namespace prefixes + * are properly registered. + */ +@Component +@Service(ValidatorProvider.class) +public class NamespaceValidatorProvider implements ValidatorProvider { + + @Override + public Validator getRootValidator(NodeState before, NodeState after) { + Validator validator = new NamespaceValidator( + Namespaces.getNamespaceMap(new ReadOnlyTree(before))); + return new SubtreeValidator(validator, JCR_SYSTEM, REP_NAMESPACES); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/Namespaces.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/Namespaces.java new file mode 100644 index 00000000000..8c94c92f259 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/Namespaces.java @@ -0,0 +1,95 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one or more +* contributor license agreements. See the NOTICE file distributed with +* this work for additional information regarding copyright ownership. +* The ASF licenses this file to You under the Apache License, Version 2.0 +* (the "License"); you may not use this file except in compliance with +* the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package org.apache.jackrabbit.oak.plugins.name; + +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.NamespaceRegistry; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; + +import static org.apache.jackrabbit.oak.api.Type.STRING; + +/** + * Internal static utility class for managing the persisted namespace registry. + */ +public class Namespaces implements NamespaceConstants { + + private Namespaces() { + } + + private static final Map DEFAULTS = new HashMap(); + static { + // Standard namespace specified by JCR (default one not included) + DEFAULTS.put(NamespaceRegistry.PREFIX_EMPTY, NamespaceRegistry.NAMESPACE_EMPTY); + DEFAULTS.put(NamespaceRegistry.PREFIX_JCR, NamespaceRegistry.NAMESPACE_JCR); + DEFAULTS.put(NamespaceRegistry.PREFIX_NT, NamespaceRegistry.NAMESPACE_NT); + DEFAULTS.put(NamespaceRegistry.PREFIX_MIX, NamespaceRegistry.NAMESPACE_MIX); + DEFAULTS.put(NamespaceRegistry.PREFIX_XML, NamespaceRegistry.NAMESPACE_XML); + + // Namespace included in Jackrabbit 2.x + DEFAULTS.put(PREFIX_SV, NAMESPACE_SV); + DEFAULTS.put(PREFIX_REP, NAMESPACE_REP); + } + + public static Map getNamespaceMap(Tree root) { + Map map = new HashMap(DEFAULTS); + + Tree system = root.getChild(JcrConstants.JCR_SYSTEM); + if (system != null) { + Tree namespaces = system.getChild(REP_NAMESPACES); + if (namespaces != null) { + for (PropertyState property : namespaces.getProperties()) { + String prefix = property.getName(); + if (!property.isArray() && isValidPrefix(prefix)) { + String value = property.getValue(STRING); + if (STRING.equals(property.getType())) { + map.put(prefix, value); + } + } + } + } + } + + return map; + } + + public static boolean isValidPrefix(String prefix) { + // TODO: Other prefix rules? + return !prefix.isEmpty() && prefix.indexOf(':') == -1; + } + + public static boolean isValidLocalName(String local) { + if (local.isEmpty() || ".".equals(local) || "..".equals(local)) { + return false; + } + + for (int i = 0; i < local.length(); i++) { + char ch = local.charAt(i); + if ("/:[]|*".indexOf(ch) != -1) { // TODO: XMLChar check + return false; + } + } + + // TODO: Other name rules? + return true; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/ReadOnlyNamespaceRegistry.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/ReadOnlyNamespaceRegistry.java new file mode 100644 index 00000000000..600f572663c --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/ReadOnlyNamespaceRegistry.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.name; + +import java.util.Arrays; +import java.util.Map; + +import javax.annotation.Nonnull; +import javax.jcr.NamespaceException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; + +import org.apache.jackrabbit.oak.api.Tree; + +/** + * Read-only namespace registry. Used mostly internally when access to the + * in-content registered namespaces is needed. See the + * {@link ReadWriteNamespaceRegistry} subclass for a more complete registry + * implementation that supports also namespace modifications and that's thus + * better suited for use in in implementing the full JCR API. + */ +public abstract class ReadOnlyNamespaceRegistry + implements NamespaceRegistry, NamespaceConstants { + + /** + * Called by the {@link NamespaceRegistry} implementation methods + * to acquire a root {@link Tree} instance from which to read the + * namespace mappings (under jcr:system/rep:namespaces). + * + * @return root {@link Tree} for reading the namespace mappings + */ + protected abstract Tree getReadTree(); + + //--------------------------------------------------< NamespaceRegistry >--- + + @Override + public void registerNamespace(String prefix, String uri) + throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + @Override + public void unregisterNamespace(String prefix) throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + @Override + @Nonnull + public String[] getPrefixes() throws RepositoryException { + try { + Tree root = getReadTree(); + Map map = Namespaces.getNamespaceMap(root); + String[] prefixes = map.keySet().toArray(new String[map.size()]); + Arrays.sort(prefixes); + return prefixes; + } catch (RuntimeException e) { + throw new RepositoryException( + "Failed to retrieve registered namespace prefixes", e); + } + } + + @Override + @Nonnull + public String[] getURIs() throws RepositoryException { + try { + Tree root = getReadTree(); + Map map = Namespaces.getNamespaceMap(root); + String[] uris = map.values().toArray(new String[map.size()]); + Arrays.sort(uris); + return uris; + } catch (RuntimeException e) { + throw new RepositoryException( + "Failed to retrieve registered namespace URIs", e); + } + } + + @Override + @Nonnull + public String getURI(String prefix) throws RepositoryException { + try { + Tree root = getReadTree(); + Map map = Namespaces.getNamespaceMap(root); + String uri = map.get(prefix); + if (uri == null) { + throw new NamespaceException( + "No namespace registered for prefix " + prefix); + } + return uri; + } catch (RuntimeException e) { + throw new RepositoryException( + "Failed to retrieve the namespace URI for prefix " + + prefix, e); + } + } + + @Override + @Nonnull + public String getPrefix(String uri) throws RepositoryException { + try { + Tree root = getReadTree(); + Map map = Namespaces.getNamespaceMap(root); + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue().equals(uri)) { + return entry.getKey(); + } + } + throw new NamespaceException( + "No namespace prefix registered for URI " + uri); + } catch (RuntimeException e) { + throw new RepositoryException( + "Failed to retrieve the namespace prefix for URI " + + uri, e); + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/ReadWriteNamespaceRegistry.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/ReadWriteNamespaceRegistry.java new file mode 100644 index 00000000000..5e084d59f4b --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/ReadWriteNamespaceRegistry.java @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.name; + +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; + +import static org.apache.jackrabbit.oak.api.Type.NAME; +import static org.apache.jackrabbit.oak.api.Type.STRING; + +/** + * Writable namespace registry. Mainly for use to implement the full JCR API. + */ +public abstract class ReadWriteNamespaceRegistry + extends ReadOnlyNamespaceRegistry { + + /** + * Called by the write methods to acquire a fresh {@link Root} instance + * that can be used to persist the requested namespace changes (and + * nothing else). + * + * @return fresh {@link Root} instance + */ + protected abstract Root getWriteRoot(); + + /** + * Called by the write methods to refresh the state of the possible + * session associated with this instance. The default implementation + * of this method does nothing, but a subclass can use this callback + * to keep a session in sync with the persisted namespace changes. + * + * @throws RepositoryException if the session could not be refreshed + */ + protected void refresh() throws RepositoryException { + // do nothing + } + + private static Tree getOrCreate(Root root, String... path) { + Tree tree = root.getTree("/"); + assert tree != null; + for (String name : path) { + Tree child = tree.getChild(name); + if (child == null) { + child = tree.addChild(name); + } + tree = child; + } + return tree; + } + + //--------------------------------------------------< NamespaceRegistry >--- + + @Override + public void registerNamespace(String prefix, String uri) + throws RepositoryException { + try { + Root root = getWriteRoot(); + Tree namespaces = + getOrCreate(root, JcrConstants.JCR_SYSTEM, REP_NAMESPACES); + if (!namespaces.hasProperty(JcrConstants.JCR_PRIMARYTYPE)) { + namespaces.setProperty(JcrConstants.JCR_PRIMARYTYPE, + JcrConstants.NT_UNSTRUCTURED, NAME); + } + // remove existing mapping to given uri + for (PropertyState p : namespaces.getProperties()) { + if (!p.isArray() && p.getValue(STRING).equals(uri)) { + namespaces.removeProperty(p.getName()); + } + } + namespaces.setProperty(prefix, uri); + root.commit(); + refresh(); + } catch (NamespaceValidatorException e) { + throw e.getNamespaceException(); + } catch (CommitFailedException e) { + throw new RepositoryException( + "Failed to register namespace mapping from " + + prefix + " to " + uri, e); + } + } + + @Override + public void unregisterNamespace(String prefix) throws RepositoryException { + Root root = getWriteRoot(); + Tree namespaces = root.getTree(NAMESPACES_PATH); + if (namespaces == null || !namespaces.hasProperty(prefix)) { + throw new NamespaceException( + "Namespace mapping from " + prefix + " to " + + getURI(prefix) + " can not be unregistered"); + } + + try { + namespaces.removeProperty(prefix); + root.commit(); + refresh(); + } catch (NamespaceValidatorException e) { + throw e.getNamespaceException(); + } catch (CommitFailedException e) { + throw new RepositoryException( + "Failed to unregister namespace mapping for prefix " + + prefix, e); + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/BuiltInNodeTypes.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/BuiltInNodeTypes.java new file mode 100644 index 00000000000..aa2f7e00ad4 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/BuiltInNodeTypes.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.plugins.nodetype; + +import java.io.InputStream; +import java.io.InputStreamReader; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; + +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.NODE_TYPES_PATH; + +/** + * BuiltInNodeTypes is a utility class that registers the built-in + * node types required for a JCR repository running on Oak. + */ +public class BuiltInNodeTypes { + + private final ReadWriteNodeTypeManager ntMgr; + + private BuiltInNodeTypes(final Root root) { + this.ntMgr = new ReadWriteNodeTypeManager() { + @Override + protected Tree getTypes() { + return root.getTree(NODE_TYPES_PATH); + } + + @Nonnull + @Override + protected Root getWriteRoot() { + return root; + } + }; + } + + /** + * Registers built in node types using the given {@link Root}. + * + * @param root the {@link Root} instance. + */ + public static void register(final Root root) { + new BuiltInNodeTypes(root).registerBuiltinNodeTypes(); + } + + private void registerBuiltinNodeTypes() { + // FIXME: migrate custom node types as well. + if (!nodeTypesInContent()) { + try { + InputStream stream = BuiltInNodeTypes.class.getResourceAsStream("builtin_nodetypes.cnd"); + try { + ntMgr.registerNodeTypes(new InputStreamReader(stream, "UTF-8")); + } finally { + stream.close(); + } + } catch (Exception e) { + throw new IllegalStateException( + "Unable to load built-in node types", e); + } + } + } + + private boolean nodeTypesInContent() { + Tree types = ntMgr.getTypes(); + return types != null && types.getChildrenCount() > 0; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/DefBuilderFactory.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/DefBuilderFactory.java new file mode 100644 index 00000000000..383d3fd7ee9 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/DefBuilderFactory.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.plugins.nodetype; + +import java.util.Map; + +import javax.jcr.nodetype.NodeTypeTemplate; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.commons.cnd.DefinitionBuilderFactory; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.namepath.NameMapperImpl; +import org.apache.jackrabbit.oak.plugins.name.NamespaceConstants; +import org.apache.jackrabbit.oak.plugins.name.Namespaces; + +class DefBuilderFactory extends + DefinitionBuilderFactory> { + + private final Tree root; + + public DefBuilderFactory(Tree root) { + this.root = root; + } + + @Override + public NodeTypeTemplateImpl newNodeTypeDefinitionBuilder() { + return new NodeTypeTemplateImpl(new NameMapperImpl(root)); + } + + @Override + public Map getNamespaceMapping() { + return Namespaces.getNamespaceMap(root); + } + + @Override + public void setNamespaceMapping(Map namespaces) { + throw new UnsupportedOperationException(); + } + + @Override + public void setNamespace(String prefix, String uri) { + if (Namespaces.getNamespaceMap(root).containsValue(uri)) { + return; // namespace already exists + } + + Tree namespaces = getOrCreate( + JcrConstants.JCR_SYSTEM, NamespaceConstants.REP_NAMESPACES); + namespaces.setProperty(prefix, uri); + } + + private Tree getOrCreate(String... path) { + Tree tree = root; + for (String name : path) { + Tree child = tree.getChild(name); + if (child == null) { + child = tree.addChild(name); + } + tree = child; + } + return tree; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/DefaultTypeEditor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/DefaultTypeEditor.java new file mode 100644 index 00000000000..59461ef682f --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/DefaultTypeEditor.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.spi.commit.CommitHook; +import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; +import org.apache.jackrabbit.oak.spi.state.DefaultNodeStateDiff; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStateUtils; + +/** + * This class updates a Lucene index when node content is changed. + */ +public class DefaultTypeEditor implements CommitHook { + + @Override + public NodeState processCommit(NodeState before, NodeState after) + throws CommitFailedException { + // TODO: Calculate default type from the node definition + NodeBuilder builder = after.builder(); + after.compareAgainstBaseState( + before, new DefaultTypeDiff(builder, "nt:unstructured")); + return builder.getNodeState(); + } + + private static class DefaultTypeDiff extends DefaultNodeStateDiff { + + private final NodeBuilder builder; + + private final String defaultType; + + public DefaultTypeDiff(NodeBuilder builder, String defaultType) { + this.builder = builder; + this.defaultType = defaultType; + } + + @Override + public void childNodeAdded(String name, NodeState after) { + if (!NodeStateUtils.isHidden(name)) { + NodeBuilder childBuilder = builder.child(name); + if (after.getProperty("jcr:primaryType") == null) { + childBuilder.setProperty("jcr:primaryType", defaultType); + } + DefaultTypeDiff childDiff = + new DefaultTypeDiff(childBuilder, defaultType); + for (ChildNodeEntry entry : after.getChildNodeEntries()) { + childDiff.childNodeAdded( + entry.getName(), entry.getNodeState()); + } + } + } + + @Override + public void childNodeChanged( + String name, NodeState before, NodeState after) { + if (!NodeStateUtils.isHidden(name)) { + NodeBuilder childBuilder = builder.child(name); + DefaultTypeDiff childDiff = + new DefaultTypeDiff(childBuilder, defaultType); + after.compareAgainstBaseState(before, childDiff); + } + } + + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/DefinitionProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/DefinitionProvider.java new file mode 100644 index 00000000000..ecffceda80d --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/DefinitionProvider.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.plugins.nodetype; + +import javax.annotation.Nonnull; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; + +/** + * DefinitionProvider... TODO + */ +public interface DefinitionProvider { + + @Nonnull + NodeDefinition getRootDefinition() throws RepositoryException; + + /** + * Returns the node definition for a child node of parent named + * nodeName with a default primary type. First the non-residual + * child node definitions of parent are checked matching the + * given node name. Then the residual definitions are checked. + * + * @param parent the parent node. + * @param nodeName the name of the child node. + * @return the applicable node definition. + * @throws RepositoryException if there is no applicable node definition + * with a default primary type. + */ + @Nonnull + NodeDefinition getDefinition(@Nonnull Node parent, @Nonnull String nodeName) + throws RepositoryException; + + @Nonnull + NodeDefinition getDefinition(Node parent, Node targetNode) throws RepositoryException; + + @Nonnull + NodeDefinition getDefinition(Iterable parentNodeTypes, String nodeName, NodeType nodeType) throws RepositoryException; + + @Nonnull + PropertyDefinition getDefinition(Node parent, Property targetProperty) throws RepositoryException; + + @Nonnull + PropertyDefinition getDefinition(Node parent, String propertyName, boolean isMultiple, int type, boolean exactTypeMatch) throws RepositoryException; + + @Nonnull + PropertyDefinition getDefinition(Iterable nodeTypes, String propertyName, boolean isMultiple, int type, boolean exactTypeMatch) throws RepositoryException; +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/EffectiveNodeTypeProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/EffectiveNodeTypeProvider.java new file mode 100644 index 00000000000..ee83a0d0cf5 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/EffectiveNodeTypeProvider.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.plugins.nodetype; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; + +import org.apache.jackrabbit.oak.api.Tree; + +/** + * EffectiveNodeTypeProvider... TODO + * + * FIXME: see also TypeValidator which has it's own private EffectiveNodeType class. See OAK-412 + */ +public interface EffectiveNodeTypeProvider { + + /** + * FIXME in contrast what the method name implies this method returns the transitive closure of the super types + * TODO clarify contract, what is the difference between this method and NodeType.getSuperTypes() + * Calculates and returns all effective node types of the given node. + * + * @param targetNode the node for which the types should be calculated. + * @return all types of the given node + * @throws RepositoryException if the type information can not be accessed + */ + Iterable getEffectiveNodeTypes(Node targetNode) throws RepositoryException; + + /** + * FIXME in contrast what the method name implies this method returns the transitive closure of the super types + * TODO clarify contract, what is the difference between this method and NodeType.getSuperTypes() + * Calculates and returns all effective node types of the given tree. + * + * @param tree + * @return all node types of the given tree + * @throws RepositoryException if the type information can not be accessed + */ + Iterable getEffectiveNodeTypes(Tree tree) throws RepositoryException; +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/InitialContent.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/InitialContent.java new file mode 100644 index 00000000000..2c19b2010cb --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/InitialContent.java @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.core.RootImpl; +import org.apache.jackrabbit.oak.spi.lifecycle.RepositoryInitializer; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeStore; +import org.apache.jackrabbit.oak.spi.state.NodeStoreBranch; + +/** + * {@code InitialContent} implements a {@link RepositoryInitializer} and + * registers built-in node types when the micro kernel becomes available. + */ +@Component +@Service(RepositoryInitializer.class) +public class InitialContent implements RepositoryInitializer { + + @Override + public void initialize(NodeStore store) { + NodeStoreBranch branch = store.branch(); + + NodeBuilder root = branch.getRoot().builder(); + root.setProperty("jcr:primaryType", "rep:root", Type.NAME); + + if (!root.hasChildNode("jcr:system")) { + NodeBuilder system = root.child("jcr:system"); + system.setProperty("jcr:primaryType", "rep:system", Type.NAME); + + system.child("jcr:versionStorage") + .setProperty("jcr:primaryType", "rep:versionStorage", Type.NAME); + system.child("jcr:nodeTypes") + .setProperty("jcr:primaryType", "rep:nodeTypes", Type.NAME); + system.child("jcr:activities") + .setProperty("jcr:primaryType", "rep:Activities", Type.NAME); + system.child("rep:privileges") + .setProperty("jcr:primaryType", "rep:Privileges", Type.NAME); + + NodeBuilder security = root.child("rep:security"); + security.setProperty( + "jcr:primaryType", "rep:AuthorizableFolder", Type.NAME); + + NodeBuilder authorizables = security.child("rep:authorizables"); + authorizables.setProperty( + "jcr:primaryType", "rep:AuthorizableFolder", Type.NAME); + + NodeBuilder users = authorizables.child("rep:users"); + users.setProperty( + "jcr:primaryType", "rep:AuthorizableFolder", Type.NAME); + + NodeBuilder a = users.child("a"); + a.setProperty("jcr:primaryType", "rep:AuthorizableFolder", Type.NAME); + + a.child("ad") + .setProperty("jcr:primaryType", "rep:AuthorizableFolder", Type.NAME) + .child("admin") + .setProperty("jcr:primaryType", "rep:User", Type.NAME) + .setProperty("jcr:uuid", "21232f29-7a57-35a7-8389-4a0e4a801fc3") + .setProperty("rep:principalName", "admin") + .setProperty("rep:authorizableId", "admin") + .setProperty("rep:password", "{SHA-256}9e515755e95513ce-1000-0696716f8baf8890a35eda1b9f2d5a4e727d1c7e1c062f03180dcc2a20f61f3b"); + + a.child("an") + .setProperty("jcr:primaryType", "rep:AuthorizableFolder", Type.NAME) + .child("anonymous") + .setProperty("jcr:primaryType", "rep:User", Type.NAME) + .setProperty("jcr:uuid", "294de355-7d9d-30b3-92d8-a1e6aab028cf") + .setProperty("rep:principalName", "anonymous") + .setProperty("rep:authorizableId", "anonymous"); + } + + if (!root.hasChildNode("oak:index")) { + NodeBuilder index = root.child("oak:index"); + index.child("uuid") + .setProperty("jcr:primaryType", "oak:queryIndexDefinition", Type.NAME) + .setProperty("type", "property") + .setProperty("propertyNames", "jcr:uuid") + .setProperty("reindex", true) + .setProperty("unique", true); + index.child("primaryType") + .setProperty("jcr:primaryType", "oak:queryIndexDefinition", Type.NAME) + .setProperty("type", "property") + .setProperty("reindex", true) + .setProperty("propertyNames", "jcr:primaryType"); + // FIXME: user-mgt related unique properties (rep:authorizableId, rep:principalName) are implementation detail and not generic for repo + // FIXME OAK-396: rep:principalName only needs to be unique if defined with user/group nodes -> add defining nt-info to uniqueness constraint otherwise ac-editing will fail. + index.child("authorizableId") + .setProperty("jcr:primaryType", "oak:queryIndexDefinition", Type.NAME) + .setProperty("type", "property") + .setProperty("propertyNames", UserConstants.REP_AUTHORIZABLE_ID) + .setProperty("reindex", true) + .setProperty("unique", true); + index.child("principalName") + .setProperty("jcr:primaryType", "oak:queryIndexDefinition", Type.NAME) + .setProperty("type", "property") + .setProperty("propertyNames", UserConstants.REP_PRINCIPAL_NAME) + .setProperty("reindex", true) + .setProperty("unique", true); + index.child("members") + .setProperty("jcr:primaryType", "oak:queryIndexDefinition", Type.NAME) + .setProperty("type", "property") + .setProperty("propertyNames", UserConstants.REP_MEMBERS) + .setProperty("reindex", true); + } + try { + branch.setRoot(root.getNodeState()); + branch.merge(); + } catch (CommitFailedException e) { + throw new RuntimeException(e); // TODO: shouldn't need the wrapper + } + + BuiltInNodeTypes.register(new RootImpl(store)); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/ItemDefinitionImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/ItemDefinitionImpl.java new file mode 100644 index 00000000000..5055c81b9a0 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/ItemDefinitionImpl.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype; + +import javax.jcr.nodetype.ItemDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.version.OnParentVersionAction; + +import org.apache.jackrabbit.oak.util.NodeUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + *
+ * [nt:{propertyDefinition,childNodeDefinition}]
+ * - jcr:name (NAME) protected 
+ * - jcr:autoCreated (BOOLEAN) protected mandatory
+ * - jcr:mandatory (BOOLEAN) protected mandatory
+ * - jcr:onParentVersion (STRING) protected mandatory
+ *     < 'COPY', 'VERSION', 'INITIALIZE', 'COMPUTE', 'IGNORE', 'ABORT'
+ * - jcr:protected (BOOLEAN) protected mandatory
+ *   ...
+ * 
+ */ +class ItemDefinitionImpl implements ItemDefinition { + + private static final Logger log = + LoggerFactory.getLogger(ItemDefinitionImpl.class); + + private final NodeType type; + + protected final NodeUtil node; + + protected ItemDefinitionImpl(NodeType type, NodeUtil node) { + this.type = type; + this.node = node; + } + + @Override + public NodeType getDeclaringNodeType() { + return type; + } + + @Override + public String getName() { + return node.getName("jcr:name", "*"); + } + + @Override + public boolean isAutoCreated() { + return node.getBoolean("jcr:autoCreated"); + } + + @Override + public boolean isMandatory() { + return node.getBoolean("jcr:mandatory"); + } + + @Override + public int getOnParentVersion() { + try { + return OnParentVersionAction.valueFromName(node.getString( + "jcr:onParentVersion", + OnParentVersionAction.ACTIONNAME_COPY)); + } catch (IllegalArgumentException e) { + log.warn("Unexpected jcr:onParentVersion value", e); + return OnParentVersionAction.COPY; + } + } + + @Override + public boolean isProtected() { + return node.getBoolean("jcr:protected"); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeDefinitionImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeDefinitionImpl.java new file mode 100644 index 00000000000..c74de9ff2a2 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeDefinitionImpl.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeManager; + +import org.apache.jackrabbit.oak.util.NodeUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + *
+ * [nt:childNodeDefinition]
+ *   ...
+ * - jcr:requiredPrimaryTypes (NAME) = 'nt:base' protected mandatory multiple
+ * - jcr:defaultPrimaryType (NAME) protected
+ * - jcr:sameNameSiblings (BOOLEAN) protected mandatory
+ * 
+ */ +class NodeDefinitionImpl extends ItemDefinitionImpl implements NodeDefinition { + + private static final Logger log = + LoggerFactory.getLogger(NodeDefinitionImpl.class); + + private final NodeTypeManager manager; + + protected NodeDefinitionImpl( + NodeTypeManager manager, NodeType type, NodeUtil node) { + super(type, node); + this.manager = manager; + } + + @Override + public String[] getRequiredPrimaryTypeNames() { + return node.getNames("jcr:requiredPrimaryTypes", "nt:base"); + } + + @Override + public NodeType[] getRequiredPrimaryTypes() { + String[] names = getRequiredPrimaryTypeNames(); + List types = new ArrayList(names.length); + for (String name : names) { + try { + types.add(manager.getNodeType(name)); + } + catch (RepositoryException e) { + log.warn("Unable to access required primary type " + + name + " of node " + getName(), e); + } + } + return types.toArray(new NodeType[types.size()]); + } + + @Override + public String getDefaultPrimaryTypeName() { + return node.getName("jcr:defaultPrimaryType", null); + } + + @Override + public NodeType getDefaultPrimaryType() { + String name = getDefaultPrimaryTypeName(); + if (name != null) { + try { + return manager.getNodeType(name); + } catch (RepositoryException e) { + log.warn("Unable to access default primary type " + + name + " of node " + getName(), e); + } + } + return null; + } + + @Override + public boolean allowsSameNameSiblings() { + return node.getBoolean("jcr:sameNameSiblings"); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeDefinitionTemplateImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeDefinitionTemplateImpl.java new file mode 100644 index 00000000000..e2fbd0c80e8 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeDefinitionTemplateImpl.java @@ -0,0 +1,220 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeDefinitionTemplate; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeTemplate; +import javax.jcr.version.OnParentVersionAction; + +import org.apache.jackrabbit.commons.cnd.DefinitionBuilderFactory.AbstractNodeDefinitionBuilder; +import org.apache.jackrabbit.oak.namepath.JcrNameParser; +import org.apache.jackrabbit.oak.namepath.NameMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class NodeDefinitionTemplateImpl + extends AbstractNodeDefinitionBuilder + implements NodeDefinitionTemplate { + + private static final Logger log = + LoggerFactory.getLogger(NodeDefinitionTemplateImpl.class); + + private String defaultPrimaryTypeName; + + private final NameMapper mapper; + private String[] requiredPrimaryTypeNames; + + protected NodeType getNodeType(String name) throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + public NodeDefinitionTemplateImpl(NameMapper mapper) { + this.mapper = mapper; + onParent = OnParentVersionAction.COPY; + } + + @Override + public void build() { + // do nothing by default + } + + @Override + public NodeType getDeclaringNodeType() { + return null; + } + + @Override + public void setDeclaringNodeType(String name) { + // ignore + } + + @Override + public void setName(String name) throws ConstraintViolationException { + JcrNameParser.checkName(name, true); + this.name = mapper.getJcrName(mapper.getOakName(name)); + } + + @Override + public boolean isAutoCreated() { + return autocreate; + } + + @Override + public void setAutoCreated(boolean autocreate) { + this.autocreate = autocreate; + } + + @Override + public boolean isProtected() { + return isProtected; + } + + @Override + public void setProtected(boolean isProtected) { + this.isProtected = isProtected; + } + + @Override + public boolean isMandatory() { + return isMandatory; + } + + @Override + public void setMandatory(boolean isMandatory) { + this.isMandatory = isMandatory; + } + + @Override + public int getOnParentVersion() { + return onParent; + } + + @Override + public void setOnParentVersion(int onParent) { + this.onParent = onParent; + } + + @Override + public boolean allowsSameNameSiblings() { + return allowSns; + } + + @Override + public void setSameNameSiblings(boolean allowSns) { + this.allowSns = allowSns; + } + + @Override + public void setAllowsSameNameSiblings(boolean allowSns) { + setSameNameSiblings(allowSns); + } + + @Override + public NodeType getDefaultPrimaryType() { + if (defaultPrimaryTypeName != null) { + try { + return getNodeType(defaultPrimaryTypeName); + } catch (RepositoryException e) { + log.warn("Unable to access default primary type " + + defaultPrimaryTypeName + " of " + name, e); + } + } + return null; + } + + @Override + public String getDefaultPrimaryTypeName() { + return defaultPrimaryTypeName; + } + + @Override + public void setDefaultPrimaryTypeName(String name) throws ConstraintViolationException { + if (name == null) { + this.defaultPrimaryTypeName = null; + } + else { + JcrNameParser.checkName(name, false); + this.defaultPrimaryTypeName = mapper.getJcrName(mapper.getOakName(name)); + } + } + + @Override + public void setDefaultPrimaryType(String name) throws ConstraintViolationException { + setDefaultPrimaryTypeName(name); + } + + @Override + public NodeType[] getRequiredPrimaryTypes() { + if (requiredPrimaryTypeNames == null) { + return null; + } else { + List types = + new ArrayList(requiredPrimaryTypeNames.length); + for (String requiredPrimaryTypeName : requiredPrimaryTypeNames) { + try { + types.add(getNodeType(requiredPrimaryTypeName)); + } + catch (RepositoryException e) { + log.warn("Unable to required primary primary type " + + requiredPrimaryTypeName + " of " + name, e); + } + } + return types.toArray(new NodeType[types.size()]); + } + } + + @Override + public String[] getRequiredPrimaryTypeNames() { + return requiredPrimaryTypeNames; + } + + @Override + public void setRequiredPrimaryTypeNames(String[] names) throws ConstraintViolationException { + if (names == null) { + throw new ConstraintViolationException("null is not a valid array of JCR names"); + } + int k = 0; + String[] n = new String[names.length]; + for (String name : names) { + JcrNameParser.checkName(name, false); + n[k++] = mapper.getJcrName(mapper.getOakName(name)); + } + this.requiredPrimaryTypeNames = n; + } + + @Override + public void addRequiredPrimaryType(String name) throws ConstraintViolationException { + JcrNameParser.checkName(name, false); + if (requiredPrimaryTypeNames == null) { + requiredPrimaryTypeNames = new String[] { mapper.getJcrName(mapper.getOakName(name)) }; + } else { + String[] names = new String[requiredPrimaryTypeNames.length + 1]; + System.arraycopy(requiredPrimaryTypeNames, 0, names, 0, requiredPrimaryTypeNames.length); + names[requiredPrimaryTypeNames.length] = mapper.getJcrName(mapper.getOakName(name)); + requiredPrimaryTypeNames = names; + } + + } + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeTypeConstants.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeTypeConstants.java new file mode 100644 index 00000000000..ab8e9282f8b --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeTypeConstants.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.plugins.nodetype; + +import org.apache.jackrabbit.JcrConstants; + +/** + * NodeTypeConstants... TODO + */ +public interface NodeTypeConstants extends JcrConstants { + + String JCR_NODE_TYPES = "jcr:nodeTypes"; + String NODE_TYPES_PATH = '/' + JcrConstants.JCR_SYSTEM + '/' + JCR_NODE_TYPES; + + String JCR_IS_ABSTRACT = "jcr:isAbstract"; + String JCR_IS_QUERYABLE = "jcr:isQueryable"; + String JCR_IS_FULLTEXT_SEARCHABLE = "jcr:isFullTextSearchable"; + String JCR_IS_QUERY_ORDERABLE = "jcr:isQueryOrderable"; + String JCR_AVAILABLE_QUERY_OPERATORS = "jcr:availableQueryOperators"; + + /** + * Additinal name constants not present in JcrConstants + */ + String JCR_CREATEDBY = "jcr:createdBy"; + String JCR_LASTMODIFIEDBY = "jcr:lastModifiedBy"; + String MIX_CREATED = "mix:created"; + String MIX_LASTMODIFIED = "mix:lastModified"; + + /** + * Merge conflict handling + */ + String MIX_REP_MERGE_CONFLICT = "rep:MergeConflict"; + String REP_OURS = "rep:ours"; + String ADD_EXISTING = "addExisting"; + String CHANGE_DELETED = "changeDeleted"; + String CHANGE_CHANGED = "changeChanged"; + String DELETE_CHANGED = "deleteChanged"; + String DELETE_DELETED = "deleteDeleted"; +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeTypeImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeTypeImpl.java new file mode 100644 index 00000000000..a57bfc828d4 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeTypeImpl.java @@ -0,0 +1,487 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Set; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.commons.iterator.NodeTypeIteratorAdapter; +import org.apache.jackrabbit.oak.namepath.JcrNameParser; +import org.apache.jackrabbit.oak.namepath.JcrPathParser; +import org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager; +import org.apache.jackrabbit.oak.plugins.nodetype.constraint.Constraints; +import org.apache.jackrabbit.oak.util.NodeUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.jackrabbit.JcrConstants.JCR_CHILDNODEDEFINITION; +import static org.apache.jackrabbit.JcrConstants.JCR_HASORDERABLECHILDNODES; +import static org.apache.jackrabbit.JcrConstants.JCR_ISMIXIN; +import static org.apache.jackrabbit.JcrConstants.JCR_NODETYPENAME; +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYITEMNAME; +import static org.apache.jackrabbit.JcrConstants.JCR_PROPERTYDEFINITION; +import static org.apache.jackrabbit.JcrConstants.JCR_SUPERTYPES; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.JCR_IS_ABSTRACT; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.JCR_IS_QUERYABLE; + +/** + *
+ * [nt:nodeType]
+ * - jcr:nodeTypeName (NAME) protected mandatory
+ * - jcr:supertypes (NAME) protected multiple
+ * - jcr:isAbstract (BOOLEAN) protected mandatory
+ * - jcr:isQueryable (BOOLEAN) protected mandatory
+ * - jcr:isMixin (BOOLEAN) protected mandatory
+ * - jcr:hasOrderableChildNodes (BOOLEAN) protected mandatory
+ * - jcr:primaryItemName (NAME) protected
+ * + jcr:propertyDefinition (nt:propertyDefinition) = nt:propertyDefinition protected sns
+ * + jcr:childNodeDefinition (nt:childNodeDefinition) = nt:childNodeDefinition protected sns
+ * 
+ */ +class NodeTypeImpl implements NodeType { + + private static final Logger log = LoggerFactory.getLogger(NodeTypeImpl.class); + + private final ReadOnlyNodeTypeManager manager; + + private final ValueFactory factory; + + private final NodeUtil node; + + public NodeTypeImpl(ReadOnlyNodeTypeManager manager, ValueFactory factory, NodeUtil node) { + this.manager = manager; + this.factory = factory; + this.node = node; + } + + @Override + public String getName() { + String name = node.getName(JCR_NODETYPENAME); + if (name == null) { + name = node.getName(); + } + return name; + } + + @Override + public String[] getDeclaredSupertypeNames() { + return node.getNames(JCR_SUPERTYPES); + } + + @Override + public boolean isAbstract() { + return node.getBoolean(JCR_IS_ABSTRACT); + } + + @Override + public boolean isMixin() { + return node.getBoolean(JCR_ISMIXIN); + } + + @Override + public boolean hasOrderableChildNodes() { + return node.getBoolean(JCR_HASORDERABLECHILDNODES); + } + + @Override + public boolean isQueryable() { + return node.getBoolean(JCR_IS_QUERYABLE); + } + + @Override + public String getPrimaryItemName() { + return node.getName(JCR_PRIMARYITEMNAME); + } + + @Override + public PropertyDefinition[] getDeclaredPropertyDefinitions() { + List nodes = node.getNodes(JCR_PROPERTYDEFINITION); + PropertyDefinition[] definitions = new PropertyDefinition[nodes.size()]; + for (int i = 0; i < nodes.size(); i++) { + definitions[i] = new PropertyDefinitionImpl( + this, factory, nodes.get(i)); + } + return definitions; + } + + @Override + public NodeDefinition[] getDeclaredChildNodeDefinitions() { + List nodes = node.getNodes(JCR_CHILDNODEDEFINITION); + NodeDefinition[] definitions = new NodeDefinition[nodes.size()]; + for (int i = 0; i < nodes.size(); i++) { + definitions[i] = new NodeDefinitionImpl(manager, this, nodes.get(i)); + } + return definitions; + } + + @Override + public NodeType[] getSupertypes() { + Collection types = new ArrayList(); + Set added = new HashSet(); + Queue queue = new LinkedList(Arrays.asList( + getDeclaredSupertypeNames())); + while (!queue.isEmpty()) { + String name = queue.remove(); + if (added.add(name)) { + try { + NodeType type = manager.getNodeType(name); + types.add(type); + queue.addAll(Arrays.asList(type.getDeclaredSupertypeNames())); + } catch (RepositoryException e) { + throw new IllegalStateException("Inconsistent node type: " + this, e); + } + } + } + return types.toArray(new NodeType[types.size()]); + } + + @Override + public NodeType[] getDeclaredSupertypes() { + String[] names = getDeclaredSupertypeNames(); + List types = new ArrayList(names.length); + for (String name : names) { + try { + NodeType type = manager.getNodeType(name); + types.add(type); + } + catch (RepositoryException e) { + log.warn("Unable to access declared supertype " + + name + " of " + getName(), e); + } + } + return types.toArray(new NodeType[types.size()]); + } + + @Override + public NodeTypeIterator getSubtypes() { + Collection types = new ArrayList(); + try { + NodeTypeIterator iterator = manager.getAllNodeTypes(); + while (iterator.hasNext()) { + NodeType type = iterator.nextNodeType(); + if (type.isNodeType(getName()) && !isNodeType(type.getName())) { + types.add(type); + } + } + } catch (RepositoryException e) { + log.warn("Unable to access subtypes of " + getName(), e); + } + return new NodeTypeIteratorAdapter(types); + } + + @Override + public NodeTypeIterator getDeclaredSubtypes() { + Collection types = new ArrayList(); + try { + NodeTypeIterator iterator = manager.getAllNodeTypes(); + while (iterator.hasNext()) { + NodeType type = iterator.nextNodeType(); + String name = type.getName(); + if (type.isNodeType(getName()) && !isNodeType(name)) { + List declaredSuperTypeNames = Arrays.asList(type.getDeclaredSupertypeNames()); + if (declaredSuperTypeNames.contains(name)) { + types.add(type); + } + } + } + } catch (RepositoryException e) { + log.warn("Unable to access declared subtypes of " + getName(), e); + } + return new NodeTypeIteratorAdapter(types); + } + + @Override + public boolean isNodeType(String nodeTypeName) { + if (nodeTypeName.equals(getName())) { + return true; + } + + for (NodeType type : getDeclaredSupertypes()) { + if (type.isNodeType(nodeTypeName)) { + return true; + } + } + + return false; + } + + @Override + public PropertyDefinition[] getPropertyDefinitions() { + // TODO distinguish between additive and overriding property definitions. See 3.7.6.8 Item Definitions in Subtypes + Collection definitions = + new ArrayList(); + for (NodeType type : getSupertypes()) { + definitions.addAll(Arrays.asList( + type.getDeclaredPropertyDefinitions())); + } + definitions.addAll(Arrays.asList(getDeclaredPropertyDefinitions())); + return definitions.toArray(new PropertyDefinition[definitions.size()]); + } + + @Override + public NodeDefinition[] getChildNodeDefinitions() { + // TODO distinguish between additive and overriding node definitions. See 3.7.6.8 Item Definitions in Subtypes + Collection definitions = + new ArrayList(); + for (NodeType type : getSupertypes()) { + definitions.addAll(Arrays.asList( + type.getDeclaredChildNodeDefinitions())); + } + definitions.addAll(Arrays.asList(getDeclaredChildNodeDefinitions())); + return definitions.toArray(new NodeDefinition[definitions.size()]); + } + + @Override + public boolean canSetProperty(String propertyName, Value value) { + if (value == null) { + return canRemoveProperty(propertyName); + } + + try { + Iterable nts = Collections.singleton((NodeType) this); + PropertyDefinition def = manager.getDefinition(nts, propertyName, false, value.getType(), false); + return !def.isProtected() && + meetsTypeConstraints(value, def.getRequiredType()) && + meetsValueConstraints(value, def.getValueConstraints()); + } catch (RepositoryException e) { // TODO don't use exceptions for flow control. Use internal method in ReadOnlyNodeTypeManager instead. + log.debug(e.getMessage()); + return false; + } + } + + @Override + public boolean canSetProperty(String propertyName, Value[] values) { + if (values == null) { + return canRemoveProperty(propertyName); + } + + try { + Iterable nts = Collections.singleton((NodeType) this); + int type = (values.length == 0) ? PropertyType.STRING : values[0].getType(); + PropertyDefinition def = manager.getDefinition(nts, propertyName, true, type, false); + return !def.isProtected() && + meetsTypeConstraints(values, def.getRequiredType()) && + meetsValueConstraints(values, def.getValueConstraints()); + } catch (RepositoryException e) { // TODO don't use exceptions for flow control. Use internal method in ReadOnlyNodeTypeManager instead. + log.debug(e.getMessage()); + return false; + } + } + + private static boolean meetsTypeConstraints(Value value, int requiredType) { + try { + switch (requiredType) { + case PropertyType.STRING: + value.getString(); + return true; + case PropertyType.BINARY: + value.getBinary(); + return true; + case PropertyType.LONG: + value.getLong(); + return true; + case PropertyType.DOUBLE: + value.getDouble(); + return true; + case PropertyType.DATE: + value.getDate(); + return true; + case PropertyType.BOOLEAN: + value.getBoolean(); + return true; + case PropertyType.NAME: { + int type = value.getType(); + return type != PropertyType.DOUBLE && + type != PropertyType.LONG && + type != PropertyType.BOOLEAN && + JcrNameParser.validate(value.getString()); + } + case PropertyType.PATH: { + int type = value.getType(); + return type != PropertyType.DOUBLE && + type != PropertyType.LONG && + type != PropertyType.BOOLEAN && + JcrPathParser.validate(value.getString()); + } + case PropertyType.REFERENCE: + case PropertyType.WEAKREFERENCE: + return IdentifierManager.isValidUUID(value.getString()); + case PropertyType.URI: + new URI(value.getString()); + return true; + case PropertyType.DECIMAL: + value.getDecimal(); + return true; + case PropertyType.UNDEFINED: + return true; + default: + log.warn("Invalid property type value: " + requiredType); + return false; + } + } + catch (RepositoryException e) { + return false; + } + catch (URISyntaxException e) { + return false; + } + } + + private static boolean meetsTypeConstraints(Value[] values, int requiredType) { + // Constraints must be met by all values + for (Value value : values) { + if (!meetsTypeConstraints(value, requiredType)) { + return false; + } + } + + return true; + } + + private static boolean meetsValueConstraints(Value value, String[] constraints) { + if (constraints == null || constraints.length == 0) { + return true; + } + + // Any of the constraints must be met + for (String constraint : constraints) { + if (Constraints.valueConstraint(value.getType(), constraint).apply(value)) { + return true; + } + } + + return false; + } + + private static boolean meetsValueConstraints(Value[] values, String[] constraints) { + if (constraints == null || constraints.length == 0) { + return true; + } + + // Constraints must be met by all values + for (Value value : values) { + if (!meetsValueConstraints(value, constraints)) { + return false; + } + } + + return true; + } + + @Override + public boolean canAddChildNode(String childNodeName) { + // FIXME: properly calculate matching definition + for (NodeDefinition definition : getChildNodeDefinitions()) { + String name = definition.getName(); + if (matches(childNodeName, name) || "*".equals(name)) { + return !definition.isProtected() && definition.getDefaultPrimaryType() != null; + } + } + return false; + } + + @Override + public boolean canAddChildNode(String childNodeName, String nodeTypeName) { + NodeType type; + try { + type = manager.getNodeType(nodeTypeName); + if (type.isAbstract()) { + return false; + } + } catch (NoSuchNodeTypeException e) { + return false; + } catch (RepositoryException e) { + log.warn("Unable to access node type " + nodeTypeName, e); + return false; + } + // FIXME: properly calculate matching definition + for (NodeDefinition definition : getChildNodeDefinitions()) { + String name = definition.getName(); + if (matches(childNodeName, name) || "*".equals(name)) { + if (definition.isProtected()) { + return false; + } + for (String required : definition.getRequiredPrimaryTypeNames()) { + if (type.isNodeType(required)) { + return true; + } + } + } + } + return false; + } + + @Override + public boolean canRemoveItem(String itemName) { + return canRemoveNode(itemName) || canRemoveProperty(itemName); + } + + @Override + public boolean canRemoveNode(String nodeName) { + // FIXME: properly calculate matching definition taking residual definitions into account. + NodeDefinition[] childNodeDefinitions = getChildNodeDefinitions(); + for (NodeDefinition definition : childNodeDefinitions) { + String name = definition.getName(); + if (matches(nodeName, name)) { + if (definition.isMandatory() || definition.isProtected()) { + return false; + } + } + } + return childNodeDefinitions.length > 0; + } + + @Override + public boolean canRemoveProperty(String propertyName) { + // FIXME: should properly calculate matching definition taking residual definitions into account. + PropertyDefinition[] propertyDefinitions = getPropertyDefinitions(); + for (PropertyDefinition definition : propertyDefinitions) { + String name = definition.getName(); + if (propertyName.equals(name)) { + if (definition.isMandatory() || definition.isProtected()) { + return false; + } + } + } + return propertyDefinitions.length > 0; + } + + private static boolean matches(String childNodeName, String name) { + // TODO need a better way to handle SNS + return childNodeName.startsWith(name); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeTypeTemplateImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeTypeTemplateImpl.java new file mode 100644 index 00000000000..b74f98af8c3 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeTypeTemplateImpl.java @@ -0,0 +1,284 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeDefinitionTemplate; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeDefinition; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeTypeTemplate; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.nodetype.PropertyDefinitionTemplate; + +import org.apache.jackrabbit.commons.cnd.DefinitionBuilderFactory.AbstractNodeTypeDefinitionBuilder; +import org.apache.jackrabbit.oak.namepath.JcrNameParser; +import org.apache.jackrabbit.oak.namepath.NameMapper; +import org.apache.jackrabbit.value.ValueFactoryImpl; + +final class NodeTypeTemplateImpl + extends AbstractNodeTypeDefinitionBuilder + implements NodeTypeTemplate { + + private final NodeTypeManager manager; + + private final NameMapper mapper; + + private final ValueFactory factory; + + private String primaryItemName; + + private String[] superTypeNames = new String[0]; + + private List propertyDefinitionTemplates; + + private List nodeDefinitionTemplates; + + public NodeTypeTemplateImpl(NodeTypeManager manager, NameMapper mapper, ValueFactory factory) { + this.manager = manager; + this.mapper = mapper; + this.factory = factory; + } + + public NodeTypeTemplateImpl(NameMapper mapper) { + this(null, mapper, ValueFactoryImpl.getInstance()); + } + + public NodeTypeTemplateImpl( + NodeTypeManager manager, NameMapper mapper, ValueFactory factory, + NodeTypeDefinition ntd) throws ConstraintViolationException { + this(manager, mapper, factory); + + setName(ntd.getName()); + setAbstract(ntd.isAbstract()); + setMixin(ntd.isMixin()); + setOrderableChildNodes(ntd.hasOrderableChildNodes()); + setQueryable(ntd.isQueryable()); + String name = ntd.getPrimaryItemName(); + if (name != null) { + setPrimaryItemName(name); + } + setDeclaredSuperTypeNames(ntd.getDeclaredSupertypeNames()); + + getPropertyDefinitionTemplates(); // Make sure propertyDefinitionTemplates is initialised + for (PropertyDefinition pd : ntd.getDeclaredPropertyDefinitions()) { + PropertyDefinitionTemplateImpl pdt = newPropertyDefinitionBuilder(); + pdt.setDeclaringNodeType(pd.getDeclaringNodeType().getName()); + pdt.setName(pd.getName()); + pdt.setProtected(pd.isProtected()); + pdt.setMandatory(pd.isMandatory()); + pdt.setAutoCreated(pd.isAutoCreated()); + pdt.setOnParentVersion(pd.getOnParentVersion()); + pdt.setMultiple(pd.isMultiple()); + pdt.setRequiredType(pd.getRequiredType()); + pdt.setDefaultValues(pd.getDefaultValues()); + pdt.setValueConstraints(pd.getValueConstraints()); + pdt.setFullTextSearchable(pd.isFullTextSearchable()); + pdt.setAvailableQueryOperators(pd.getAvailableQueryOperators()); + pdt.setQueryOrderable(pd.isQueryOrderable()); + pdt.build(); + } + + getNodeDefinitionTemplates(); // Make sure nodeDefinitionTemplates is initialised + for (NodeDefinition nd : ntd.getDeclaredChildNodeDefinitions()) { + NodeDefinitionTemplateImpl ndt = newNodeDefinitionBuilder(); + ndt.setDeclaringNodeType(nd.getDeclaringNodeType().getName()); + ndt.setName(nd.getName()); + ndt.setProtected(nd.isProtected()); + ndt.setMandatory(nd.isMandatory()); + ndt.setAutoCreated(nd.isAutoCreated()); + ndt.setOnParentVersion(nd.getOnParentVersion()); + ndt.setSameNameSiblings(nd.allowsSameNameSiblings()); + ndt.setDefaultPrimaryTypeName(nd.getDefaultPrimaryTypeName()); + ndt.setRequiredPrimaryTypeNames(nd.getRequiredPrimaryTypeNames()); + ndt.build(); + } + } + + @Override + public NodeTypeTemplate build() { + return this; + } + + @Override + public PropertyDefinitionTemplateImpl newPropertyDefinitionBuilder() { + return new PropertyDefinitionTemplateImpl(mapper) { + @Override + protected Value createValue(String value) { + return factory.createValue(value); + } + @Override + public void build() { + getPropertyDefinitionTemplates().add(this); + } + }; + } + + @Override + public NodeDefinitionTemplateImpl newNodeDefinitionBuilder() { + return new NodeDefinitionTemplateImpl(mapper) { + @Override + protected NodeType getNodeType(String name) + throws RepositoryException { + if (manager != null) { + return manager.getNodeType(name); + } else { + return super.getNodeType(name); + } + } + @Override + public void build() { + getNodeDefinitionTemplates().add(this); + } + }; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) throws ConstraintViolationException { + JcrNameParser.checkName(name, false); + this.name = mapper.getJcrName(mapper.getOakName(name)); + } + + @Override + public boolean isAbstract() { + return isAbstract; + } + + @Override + public void setAbstract(boolean abstractStatus) { + this.isAbstract = abstractStatus; + } + + @Override + public boolean isMixin() { + return isMixin; + } + + @Override + public void setMixin(boolean mixin) { + this.isMixin = mixin; + } + + @Override + public boolean hasOrderableChildNodes() { + return isOrderable ; + } + + @Override + public void setOrderableChildNodes(boolean orderable) { + this.isOrderable = orderable; + } + + @Override + public boolean isQueryable() { + return queryable; + } + + @Override + public void setQueryable(boolean queryable) { + this.queryable = queryable; + } + + @Override + public String getPrimaryItemName() { + return primaryItemName ; + } + + @Override + public void setPrimaryItemName(String name) throws ConstraintViolationException { + if (name == null) { + this.primaryItemName = null; + } + else { + JcrNameParser.checkName(name, false); + this.primaryItemName = mapper.getJcrName(mapper.getOakName(name)); + } + } + + @Override + public String[] getDeclaredSupertypeNames() { + return superTypeNames; + } + + @Override + public void setDeclaredSuperTypeNames(String[] names) throws ConstraintViolationException { + if (names == null) { + throw new ConstraintViolationException("null is not a valid array of JCR names"); + } + int k = 0; + String[] n = new String[names.length]; + for (String name : names) { + JcrNameParser.checkName(name, false); + n[k++] = mapper.getJcrName(mapper.getOakName(name)); + } + this.superTypeNames = n; + } + + @Override + public void addSupertype(String name) throws RepositoryException { + JcrNameParser.checkName(name, false); + String[] names = new String[superTypeNames.length + 1]; + System.arraycopy(superTypeNames, 0, names, 0, superTypeNames.length); + names[superTypeNames.length] = mapper.getJcrName(mapper.getOakName(name)); + superTypeNames = names; + } + + @Override + public List getPropertyDefinitionTemplates() { + if (propertyDefinitionTemplates == null) { + propertyDefinitionTemplates = new ArrayList(); + } + return propertyDefinitionTemplates; + } + + @Override + public List getNodeDefinitionTemplates() { + if (nodeDefinitionTemplates == null) { + nodeDefinitionTemplates = new ArrayList(); + } + return nodeDefinitionTemplates; + } + + @Override + public PropertyDefinition[] getDeclaredPropertyDefinitions() { + return propertyDefinitionTemplates == null + ? null + : propertyDefinitionTemplates.toArray( + new PropertyDefinition[propertyDefinitionTemplates.size()]); + } + + @Override + public NodeDefinition[] getDeclaredChildNodeDefinitions() { + return nodeDefinitionTemplates == null + ? null + : nodeDefinitionTemplates.toArray( + new NodeDefinition[nodeDefinitionTemplates.size()]); + } + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/PropertyDefinitionImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/PropertyDefinitionImpl.java new file mode 100644 index 00000000000..318c024abff --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/PropertyDefinitionImpl.java @@ -0,0 +1,186 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype; + +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.query.qom.QueryObjectModelConstants; + +import org.apache.jackrabbit.oak.util.NodeUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static javax.jcr.PropertyType.BINARY; +import static javax.jcr.PropertyType.BOOLEAN; +import static javax.jcr.PropertyType.DATE; +import static javax.jcr.PropertyType.DECIMAL; +import static javax.jcr.PropertyType.DOUBLE; +import static javax.jcr.PropertyType.LONG; +import static javax.jcr.PropertyType.NAME; +import static javax.jcr.PropertyType.PATH; +import static javax.jcr.PropertyType.REFERENCE; +import static javax.jcr.PropertyType.STRING; +import static javax.jcr.PropertyType.TYPENAME_BINARY; +import static javax.jcr.PropertyType.TYPENAME_BOOLEAN; +import static javax.jcr.PropertyType.TYPENAME_DATE; +import static javax.jcr.PropertyType.TYPENAME_DECIMAL; +import static javax.jcr.PropertyType.TYPENAME_DOUBLE; +import static javax.jcr.PropertyType.TYPENAME_LONG; +import static javax.jcr.PropertyType.TYPENAME_NAME; +import static javax.jcr.PropertyType.TYPENAME_PATH; +import static javax.jcr.PropertyType.TYPENAME_REFERENCE; +import static javax.jcr.PropertyType.TYPENAME_STRING; +import static javax.jcr.PropertyType.TYPENAME_UNDEFINED; +import static javax.jcr.PropertyType.TYPENAME_URI; +import static javax.jcr.PropertyType.TYPENAME_WEAKREFERENCE; +import static javax.jcr.PropertyType.UNDEFINED; +import static javax.jcr.PropertyType.URI; +import static javax.jcr.PropertyType.WEAKREFERENCE; + +/** + *
+ * [nt:propertyDefinition]
+ *   ...
+ * - jcr:requiredType (STRING) protected mandatory
+ *   < 'STRING', 'URI', 'BINARY', 'LONG', 'DOUBLE',
+ *     'DECIMAL', 'BOOLEAN', 'DATE', 'NAME', 'PATH',
+ *     'REFERENCE', 'WEAKREFERENCE', 'UNDEFINED'
+ * - jcr:valueConstraints (STRING) protected multiple
+ * - jcr:defaultValues (UNDEFINED) protected multiple
+ * - jcr:multiple (BOOLEAN) protected mandatory
+ * - jcr:availableQueryOperators (NAME) protected mandatory multiple
+ * - jcr:isFullTextSearchable (BOOLEAN) protected mandatory
+ * - jcr:isQueryOrderable (BOOLEAN) protected mandatory
+ * 
+ */ +class PropertyDefinitionImpl extends ItemDefinitionImpl + implements PropertyDefinition { + + private static final Logger log = + LoggerFactory.getLogger(PropertyDefinitionImpl.class); + + private final ValueFactory factory; + + public PropertyDefinitionImpl(NodeType type, ValueFactory factory, NodeUtil node) { + super(type, node); + this.factory = factory; + } + + /** + * Returns the numeric constant value of the type with the specified name. + * + * In contrast to {@link javax.jcr.PropertyType#valueFromName(String)} this method + * requires all type names to be all upper case. + * See also: OAK-294 and http://java.net/jira/browse/JSR_283-811 + * + * @param name the name of the property type. + * @return the numeric constant value. + * @throws IllegalArgumentException if {@code name} is not a valid property type name. + */ + public static int valueFromName(String name) { + if (name.equals(TYPENAME_STRING.toUpperCase())) { + return STRING; + } else if (name.equals(TYPENAME_BINARY.toUpperCase())) { + return BINARY; + } else if (name.equals(TYPENAME_BOOLEAN.toUpperCase())) { + return BOOLEAN; + } else if (name.equals(TYPENAME_LONG.toUpperCase())) { + return LONG; + } else if (name.equals(TYPENAME_DOUBLE.toUpperCase())) { + return DOUBLE; + } else if (name.equals(TYPENAME_DECIMAL.toUpperCase())) { + return DECIMAL; + } else if (name.equals(TYPENAME_DATE.toUpperCase())) { + return DATE; + } else if (name.equals(TYPENAME_NAME.toUpperCase())) { + return NAME; + } else if (name.equals(TYPENAME_PATH.toUpperCase())) { + return PATH; + } else if (name.equals(TYPENAME_REFERENCE.toUpperCase())) { + return REFERENCE; + } else if (name.equals(TYPENAME_WEAKREFERENCE.toUpperCase())) { + return WEAKREFERENCE; + } else if (name.equals(TYPENAME_URI.toUpperCase())) { + return URI; + } else if (name.equals(TYPENAME_UNDEFINED.toUpperCase())) { + return UNDEFINED; + } else { + throw new IllegalArgumentException("unknown type: " + name); + } + } + + @Override + public int getRequiredType() { + try { + return valueFromName(node.getString("jcr:requiredType", TYPENAME_UNDEFINED)); + } catch (IllegalArgumentException e) { + log.warn("Unexpected jcr:requiredType value", e); + return UNDEFINED; + } + } + + @Override + public String[] getValueConstraints() { + // TODO: namespace mapping? + return node.getStrings("jcr:valueConstraints"); + } + + @Override + public Value[] getDefaultValues() { + if (factory != null) { + return node.getValues("jcr:defaultValues", factory); + } + else { + log.warn("Cannot create default values: no value factory"); + return null; + } + } + + @Override + public boolean isMultiple() { + return node.getBoolean("jcr:multiple"); + } + + @Override + public String[] getAvailableQueryOperators() { + String[] ops = node.getStrings("jcr:availableQueryOperators"); + if (ops == null) { + ops = new String[] { + QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, + QueryObjectModelConstants.JCR_OPERATOR_NOT_EQUAL_TO, + QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN, + QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO, + QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN, + QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO, + QueryObjectModelConstants.JCR_OPERATOR_LIKE }; + } + return ops; + } + + @Override + public boolean isFullTextSearchable() { + return node.getBoolean("jcr:isFullTextSearchable"); + } + + @Override + public boolean isQueryOrderable() { + return node.getBoolean("jcr:isQueryOrderable"); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/PropertyDefinitionTemplateImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/PropertyDefinitionTemplateImpl.java new file mode 100644 index 00000000000..4e842dfac37 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/PropertyDefinitionTemplateImpl.java @@ -0,0 +1,202 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeTemplate; +import javax.jcr.nodetype.PropertyDefinitionTemplate; +import javax.jcr.version.OnParentVersionAction; + +import org.apache.jackrabbit.commons.cnd.DefinitionBuilderFactory.AbstractPropertyDefinitionBuilder; +import org.apache.jackrabbit.oak.namepath.JcrNameParser; +import org.apache.jackrabbit.oak.namepath.NameMapper; + +class PropertyDefinitionTemplateImpl + extends AbstractPropertyDefinitionBuilder + implements PropertyDefinitionTemplate { + + private String[] valueConstraints; + + private final NameMapper mapper; + private Value[] defaultValues; + + public PropertyDefinitionTemplateImpl(NameMapper mapper) { + this.mapper = mapper; + onParent = OnParentVersionAction.COPY; + requiredType = PropertyType.STRING; + } + + protected Value createValue(String value) throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + @Override + public void build() { + // do nothing by default + } + + @Override + public NodeType getDeclaringNodeType() { + return null; + } + + @Override + public void setDeclaringNodeType(String name) { + // ignore + } + + @Override + public void setName(String name) throws ConstraintViolationException { + JcrNameParser.checkName(name, true); + this.name = mapper.getJcrName(mapper.getOakName(name)); + } + + @Override + public boolean isAutoCreated() { + return autocreate; + } + + @Override + public void setAutoCreated(boolean autocreate) { + this.autocreate = autocreate; + } + + @Override + public boolean isProtected() { + return isProtected; + } + + @Override + public void setProtected(boolean isProtected) { + this.isProtected = isProtected; + } + + @Override + public boolean isMandatory() { + return isMandatory; + } + + @Override + public void setMandatory(boolean isMandatory) { + this.isMandatory = isMandatory; + } + + @Override + public int getOnParentVersion() { + return onParent; + } + + @Override + public void setOnParentVersion(int onParent) { + this.onParent = onParent; + } + + @Override + public void setRequiredType(int requiredType) { + this.requiredType = requiredType; + } + + @Override + public boolean isMultiple() { + return isMultiple; + } + + @Override + public void setMultiple(boolean isMultiple) { + this.isMultiple = isMultiple; + } + + @Override + public boolean isQueryOrderable() { + return queryOrderable; + } + + @Override + public void setQueryOrderable(boolean queryOrderable) { + this.queryOrderable = queryOrderable; + } + + @Override + public boolean isFullTextSearchable() { + return fullTextSearchable; + } + + @Override + public void setFullTextSearchable(boolean fullTextSearchable) { + this.fullTextSearchable = fullTextSearchable; + } + + @Override + public String[] getAvailableQueryOperators() { + return queryOperators; + } + + @Override + public void setAvailableQueryOperators(String[] queryOperators) { + this.queryOperators = queryOperators; + } + + @Override + public Value[] getDefaultValues() { + return defaultValues; + } + + @Override + public void setDefaultValues(Value[] defaultValues) { + this.defaultValues = defaultValues; + } + + @Override + public void addDefaultValues(String value) throws RepositoryException { + if (defaultValues == null) { + defaultValues = new Value[] { createValue(value) }; + } else { + Value[] values = new Value[defaultValues.length + 1]; + System.arraycopy(defaultValues, 0, values, 0, defaultValues.length); + values[defaultValues.length] = createValue(value); + defaultValues = values; + } + } + + @Override + public String[] getValueConstraints() { + return valueConstraints; + } + + @Override + public void setValueConstraints(String[] constraints) { + this.valueConstraints = constraints; + } + + @Override + public void addValueConstraint(String constraint) { + if (valueConstraints == null) { + valueConstraints = new String[] { constraint }; + } else { + String[] constraints = new String[valueConstraints.length + 1]; + System.arraycopy(valueConstraints, 0, constraints, 0, valueConstraints.length); + constraints[valueConstraints.length] = constraint; + valueConstraints = constraints; + } + } + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/ReadOnlyNodeTypeManager.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/ReadOnlyNodeTypeManager.java new file mode 100644 index 00000000000..d386269d276 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/ReadOnlyNodeTypeManager.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.plugins.nodetype; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Queue; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeDefinitionTemplate; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeDefinition; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeTypeTemplate; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.nodetype.PropertyDefinitionTemplate; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Queues; +import org.apache.jackrabbit.commons.iterator.NodeTypeIteratorAdapter; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.namepath.NameMapper; +import org.apache.jackrabbit.oak.namepath.NamePathMapperImpl; +import org.apache.jackrabbit.oak.util.NodeUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkNotNull; +import static javax.jcr.PropertyType.UNDEFINED; +import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES; +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; +import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED; +import static org.apache.jackrabbit.oak.api.Type.STRING; +import static org.apache.jackrabbit.oak.api.Type.STRINGS; + +/** + * Base implementation of a {@link NodeTypeManager} with support for reading + * node types from the {@link Tree} returned by {@link #getTypes()}. Methods + * related to node type modifications throw + * {@link UnsupportedRepositoryOperationException}. + */ +public abstract class ReadOnlyNodeTypeManager implements NodeTypeManager, EffectiveNodeTypeProvider, DefinitionProvider { + + private static final Logger log = LoggerFactory.getLogger(ReadOnlyNodeTypeManager.class); + + /** + * Returns the internal name for the specified JCR name. + * + * @param jcrName JCR node type name. + * @return the internal representation of the given JCR name. + * @throws javax.jcr.RepositoryException If there is no valid internal representation + * of the specified JCR name. + */ + @Nonnull + protected final String getOakName(String jcrName) throws RepositoryException { + String oakName = getNameMapper().getOakName(jcrName); + if (oakName == null) { + throw new RepositoryException("Invalid JCR name " + jcrName); + } + return oakName; + } + + /** + * @return {@link org.apache.jackrabbit.oak.api.Tree} instance where the node types + * are stored or {@code null} if none. + */ + @CheckForNull + protected abstract Tree getTypes(); + + /** + * The value factory to be used by {@link org.apache.jackrabbit.oak.plugins.nodetype.PropertyDefinitionImpl#getDefaultValues()}. + * If {@code null} the former returns {@code null}. + * @return {@code ValueFactory} instance or {@code null}. + */ + @CheckForNull + protected ValueFactory getValueFactory() { + return null; + } + + /** + * Returns a {@link NameMapper} to be used by this node type manager. This + * implementation returns the {@link NamePathMapperImpl#DEFAULT} instance. A + * subclass may override this method and provide a different + * implementation. + * + * @return {@link NameMapper} instance. + */ + @Nonnull + protected NameMapper getNameMapper() { + return NamePathMapperImpl.DEFAULT; + } + + //----------------------------------------------------< NodeTypeManager >--- + + @Override + public boolean hasNodeType(String name) throws RepositoryException { + Tree types = getTypes(); + return types != null && types.hasChild(getOakName(name)); + } + + @Override + public NodeType getNodeType(String name) throws RepositoryException { + Tree types = getTypes(); + if (types != null) { + Tree type = types.getChild(getOakName(name)); + if (type != null) { + return new NodeTypeImpl(this, getValueFactory(), + new NodeUtil(type, getNameMapper())); + } + } + throw new NoSuchNodeTypeException(name); + } + + @Override + public NodeTypeIterator getAllNodeTypes() throws RepositoryException { + List list = Lists.newArrayList(); + Tree types = getTypes(); + if (types != null) { + for (Tree type : types.getChildren()) { + list.add(new NodeTypeImpl(this, getValueFactory(), + new NodeUtil(type, getNameMapper()))); + + } + } + return new NodeTypeIteratorAdapter(list); + } + + @Override + public NodeTypeIterator getPrimaryNodeTypes() throws RepositoryException { + List list = Lists.newArrayList(); + NodeTypeIterator iterator = getAllNodeTypes(); + while (iterator.hasNext()) { + NodeType type = iterator.nextNodeType(); + if (!type.isMixin()) { + list.add(type); + } + } + return new NodeTypeIteratorAdapter(list); + } + + @Override + public NodeTypeIterator getMixinNodeTypes() throws RepositoryException { + List list = Lists.newArrayList(); + NodeTypeIterator iterator = getAllNodeTypes(); + while (iterator.hasNext()) { + NodeType type = iterator.nextNodeType(); + if (type.isMixin()) { + list.add(type); + } + } + return new NodeTypeIteratorAdapter(list); + } + + @Override + public NodeTypeTemplate createNodeTypeTemplate() throws RepositoryException { + return new NodeTypeTemplateImpl(this, getNameMapper(), getValueFactory()); + } + + @Override + public NodeTypeTemplate createNodeTypeTemplate(NodeTypeDefinition ntd) throws RepositoryException { + return new NodeTypeTemplateImpl(this, getNameMapper(), getValueFactory(), ntd); + } + + @Override + public NodeDefinitionTemplate createNodeDefinitionTemplate() { + return new NodeDefinitionTemplateImpl(getNameMapper()); + } + + @Override + public PropertyDefinitionTemplate createPropertyDefinitionTemplate() { + return new PropertyDefinitionTemplateImpl(getNameMapper()); + } + + /** + * This implementation always throws a {@link UnsupportedRepositoryOperationException}. + */ + @Override + public NodeType registerNodeType(NodeTypeDefinition ntd, boolean allowUpdate) throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * This implementation always throws a {@link UnsupportedRepositoryOperationException}. + */ + @Override + public NodeTypeIterator registerNodeTypes(NodeTypeDefinition[] ntds, boolean allowUpdate) throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * This implementation always throws a {@link UnsupportedRepositoryOperationException}. + */ + @Override + public void unregisterNodeType(String name) throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * This implementation always throws a {@link UnsupportedRepositoryOperationException}. + */ + @Override + public void unregisterNodeTypes(String[] names) throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + //------------------------------------------< EffectiveNodeTypeProvider >--- + + /** + * Returns all the node types of the given node, in a breadth-first + * traversal order of the type hierarchy. + * + * @param node node instance + * @return all types of the given node + * @throws RepositoryException if the type information can not be accessed + * @param node + * @return + * @throws RepositoryException + */ + @Override + public Iterable getEffectiveNodeTypes(Node node) throws RepositoryException { + Queue queue = Queues.newArrayDeque(); + queue.add(node.getPrimaryNodeType()); + queue.addAll(Arrays.asList(node.getMixinNodeTypes())); + + return getEffectiveNodeTypes(queue); + } + + @Override + public Iterable getEffectiveNodeTypes(Tree tree) throws RepositoryException { + Queue queue = Queues.newArrayDeque(); + + NodeType primaryType; + PropertyState jcrPrimaryType = tree.getProperty(JCR_PRIMARYTYPE); + if (jcrPrimaryType != null) { + String ntName = jcrPrimaryType.getValue(STRING); + primaryType = getNodeType(ntName); + } else { + log.warn("Item at {} has no primary type. Assuming nt:unstructured", tree.getPath()); + primaryType = getNodeType(NT_UNSTRUCTURED); + } + queue.add(primaryType); + + List mixinTypes = Lists.newArrayList(); + PropertyState jcrMixinType = tree.getProperty(JCR_MIXINTYPES); + if (jcrMixinType != null) { + for (String ntName : jcrMixinType.getValue(STRINGS)) { + mixinTypes.add(getNodeType(ntName)); + } + } + queue.addAll(mixinTypes); + + return getEffectiveNodeTypes(queue); + } + + //-------------------------------------------------< DefinitionProvider >--- + + @Override + public NodeDefinition getRootDefinition() throws RepositoryException { + return new RootNodeDefinition(this); + } + + @Nonnull + @Override + public NodeDefinition getDefinition(@Nonnull Node parent, + @Nonnull String nodeName) + throws RepositoryException { + checkNotNull(parent); + checkNotNull(nodeName); + List residualDefs = new ArrayList(); + for (NodeType nt : getEffectiveNodeTypes(parent)) { + for (NodeDefinition def : nt.getDeclaredChildNodeDefinitions()) { + String defName = def.getName(); + if (nodeName.equals(defName) + && def.getDefaultPrimaryTypeName() != null) { + return def; + } else if ("*".equals(defName)) { + residualDefs.add(def); + } + } + } + + for (NodeDefinition def : residualDefs) { + if (def.getDefaultPrimaryTypeName() != null) { + // return the first definition with a default primary type + return def; + } + } + + throw new RepositoryException("No matching node definition found for " + this); + } + + @Override + public NodeDefinition getDefinition(@Nonnull Node parent, @Nonnull Node targetNode) throws RepositoryException { + String name = targetNode.getName(); + List residualDefs = new ArrayList(); + // TODO: This may need to be optimized + for (NodeType nt : getEffectiveNodeTypes(parent)) { + for (NodeDefinition def : nt.getDeclaredChildNodeDefinitions()) { + String defName = def.getName(); + if (name.equals(defName)) { + boolean match = true; + for (String type : def.getRequiredPrimaryTypeNames()) { + if (!targetNode.isNodeType(type)) { + match = false; + } + } + if (match) { + return def; + } + } else if ("*".equals(defName)) { + residualDefs.add(def); + } + } + } + + for (NodeDefinition def : residualDefs) { + String defName = def.getName(); + if ("*".equals(defName)) { + boolean match = true; + for (String type : def.getRequiredPrimaryTypeNames()) { + if (!targetNode.isNodeType(type)) { + match = false; + } + } + if (match) { + return def; + } + } + } + + throw new RepositoryException("No matching node definition found for " + this); + } + + @Override + public NodeDefinition getDefinition(Iterable parentNodeTypes, String nodeName, NodeType nodeType) throws RepositoryException { + List residualDefs = new ArrayList(); + // TODO: This may need to be optimized + // TODO: cleanup redundancy with getDefinition(Node, Node) + for (NodeType nt : parentNodeTypes) { + for (NodeDefinition def : nt.getDeclaredChildNodeDefinitions()) { + String defName = def.getName(); + if (nodeName.equals(defName)) { + boolean match = true; + // TODO: check again if passing null nodeType is legal. + if (nodeType != null) { + for (String type : def.getRequiredPrimaryTypeNames()) { + if (!nodeType.isNodeType(type)) { + match = false; + } + } + } + if (match) { + return def; + } + } else if ("*".equals(defName)) { + residualDefs.add(def); + } + } + } + + for (NodeDefinition def : residualDefs) { + String defName = def.getName(); + if ("*".equals(defName)) { + boolean match = true; + for (String type : def.getRequiredPrimaryTypeNames()) { + if (!nodeType.isNodeType(type)) { + match = false; + } + } + if (match) { + return def; + } + } + } + + throw new RepositoryException("No matching node definition found for " + this); + } + + @Override + public PropertyDefinition getDefinition(Node parent, Property targetProperty) throws RepositoryException { + String name = targetProperty.getName(); + boolean isMultiple = targetProperty.isMultiple(); + int type = UNDEFINED; + if (isMultiple) { + Value[] values = targetProperty.getValues(); + if (values.length > 0) { + type = values[0].getType(); + } + } else { + type = targetProperty.getValue().getType(); + } + + // TODO: This may need to be optimized + List residualDefs = new ArrayList(); + for (NodeType nt : getEffectiveNodeTypes(parent)) { + for (PropertyDefinition def : nt.getDeclaredPropertyDefinitions()) { + String defName = def.getName(); + int defType = def.getRequiredType(); + if ((name.equals(defName)) + && (type == defType || UNDEFINED == type || UNDEFINED == defType) + && isMultiple == def.isMultiple()) { + return def; + } else if ("*".equals(defName)) { + residualDefs.add(def); + } + } + } + + for (PropertyDefinition def : residualDefs) { + String defName = def.getName(); + int defType = def.getRequiredType(); + if (("*".equals(defName)) + && (type == defType || UNDEFINED == type || UNDEFINED == defType) + && isMultiple == def.isMultiple()) { + return def; + } + } + + // FIXME: Shouldn't be needed + for (NodeType nt : getEffectiveNodeTypes(parent)) { + for (PropertyDefinition def : nt.getDeclaredPropertyDefinitions()) { + String defName = def.getName(); + if ((name.equals(defName) || "*".equals(defName)) + && type == PropertyType.STRING + && isMultiple == def.isMultiple()) { + return def; + } + } + } + + throw new RepositoryException("No matching property definition found for " + this); + } + + @Override + public PropertyDefinition getDefinition(Node parent, String propertyName, boolean isMultiple, int type, boolean exactTypeMatch) throws RepositoryException { + return getPropertyDefinition(getEffectiveNodeTypes(parent), propertyName, isMultiple, type, exactTypeMatch); + } + + @Override + public PropertyDefinition getDefinition(Iterable nodeTypes, String propertyName, boolean isMultiple, + int type, boolean exactTypeMatch) throws RepositoryException { + Queue queue = Queues.newArrayDeque(nodeTypes); + Collection effective = getEffectiveNodeTypes(queue); + return getPropertyDefinition(effective, propertyName, isMultiple, type, exactTypeMatch); + } + + //------------------------------------------------------------< private >--- + + private static Collection getEffectiveNodeTypes(Queue queue) { + Map types = Maps.newHashMap(); + while (!queue.isEmpty()) { + NodeType type = queue.remove(); + String name = type.getName(); + if (!types.containsKey(name)) { + types.put(name, type); + queue.addAll(Arrays.asList(type.getDeclaredSupertypes())); + } + } + + return types.values(); + } + + private static PropertyDefinition getPropertyDefinition(Iterable effectiveNodeTypes, + String propertyName, boolean isMultiple, + int type, boolean exactTypeMatch) throws RepositoryException { + // TODO: This may need to be optimized + for (NodeType nt : effectiveNodeTypes) { + for (PropertyDefinition def : nt.getDeclaredPropertyDefinitions()) { + String defName = def.getName(); + int defType = def.getRequiredType(); + if (propertyName.equals(defName) + && isMultiple == def.isMultiple() + &&(!exactTypeMatch || (type == defType || UNDEFINED == type || UNDEFINED == defType))) { + return def; + } + } + } + + // try if there is a residual definition + for (NodeType nt : effectiveNodeTypes) { + for (PropertyDefinition def : nt.getDeclaredPropertyDefinitions()) { + String defName = def.getName(); + int defType = def.getRequiredType(); + if ("*".equals(defName) + && isMultiple == def.isMultiple() + && (!exactTypeMatch || (type == defType || UNDEFINED == type || UNDEFINED == defType))) { + return def; + } + } + } + + // FIXME: Shouldn't be needed + for (NodeType nt : effectiveNodeTypes) { + for (PropertyDefinition def : nt.getDeclaredPropertyDefinitions()) { + String defName = def.getName(); + if ((propertyName.equals(defName) || "*".equals(defName)) + && type == PropertyType.STRING + && isMultiple == def.isMultiple()) { + return def; + } + } + } + throw new RepositoryException("No matching property definition found for " + propertyName); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/ReadWriteNodeTypeManager.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/ReadWriteNodeTypeManager.java new file mode 100644 index 00000000000..4447ab88d49 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/ReadWriteNodeTypeManager.java @@ -0,0 +1,399 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype; + +import java.io.Reader; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nonnull; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.nodetype.ItemDefinition; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeDefinition; +import javax.jcr.nodetype.NodeTypeExistsException; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeTemplate; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.version.OnParentVersionAction; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.apache.jackrabbit.commons.cnd.CompactNodeTypeDefReader; +import org.apache.jackrabbit.commons.cnd.ParseException; +import org.apache.jackrabbit.commons.iterator.NodeTypeIteratorAdapter; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.util.NodeUtil; + +import static org.apache.jackrabbit.JcrConstants.JCR_AUTOCREATED; +import static org.apache.jackrabbit.JcrConstants.JCR_CHILDNODEDEFINITION; +import static org.apache.jackrabbit.JcrConstants.JCR_DEFAULTPRIMARYTYPE; +import static org.apache.jackrabbit.JcrConstants.JCR_DEFAULTVALUES; +import static org.apache.jackrabbit.JcrConstants.JCR_HASORDERABLECHILDNODES; +import static org.apache.jackrabbit.JcrConstants.JCR_ISMIXIN; +import static org.apache.jackrabbit.JcrConstants.JCR_MANDATORY; +import static org.apache.jackrabbit.JcrConstants.JCR_MULTIPLE; +import static org.apache.jackrabbit.JcrConstants.JCR_NAME; +import static org.apache.jackrabbit.JcrConstants.JCR_NODETYPENAME; +import static org.apache.jackrabbit.JcrConstants.JCR_ONPARENTVERSION; +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYITEMNAME; +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; +import static org.apache.jackrabbit.JcrConstants.JCR_PROPERTYDEFINITION; +import static org.apache.jackrabbit.JcrConstants.JCR_PROTECTED; +import static org.apache.jackrabbit.JcrConstants.JCR_REQUIREDPRIMARYTYPES; +import static org.apache.jackrabbit.JcrConstants.JCR_REQUIREDTYPE; +import static org.apache.jackrabbit.JcrConstants.JCR_SAMENAMESIBLINGS; +import static org.apache.jackrabbit.JcrConstants.JCR_SUPERTYPES; +import static org.apache.jackrabbit.JcrConstants.JCR_SYSTEM; +import static org.apache.jackrabbit.JcrConstants.JCR_VALUECONSTRAINTS; +import static org.apache.jackrabbit.JcrConstants.NT_BASE; +import static org.apache.jackrabbit.JcrConstants.NT_CHILDNODEDEFINITION; +import static org.apache.jackrabbit.JcrConstants.NT_NODETYPE; +import static org.apache.jackrabbit.JcrConstants.NT_PROPERTYDEFINITION; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.JCR_AVAILABLE_QUERY_OPERATORS; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.JCR_IS_ABSTRACT; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.JCR_IS_FULLTEXT_SEARCHABLE; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.JCR_IS_QUERYABLE; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.JCR_IS_QUERY_ORDERABLE; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.JCR_NODE_TYPES; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.NODE_TYPES_PATH; + +/** + * {@code ReadWriteNodeTypeManager} extends the {@link ReadOnlyNodeTypeManager} + * and add support for operations that modify node types: + *
    + *
  • {@link #registerNodeType(NodeTypeDefinition, boolean)}
  • + *
  • {@link #registerNodeTypes(NodeTypeDefinition[], boolean)}
  • + *
  • {@link #unregisterNodeType(String)}
  • + *
  • {@link #unregisterNodeTypes(String[])}
  • + *
+ * Calling any of the above methods will result in a {@link #refresh()} callback + * to e.g. inform an associated session that it should refresh to make the + * changes visible. + *

+ * Subclass responsibility is to provide an implementation of + * {@link #getTypes()} for read only access to the tree where node types are + * stored in content and {@link #getWriteRoot()} for write access to the + * repository in order to modify node types stored in content. A subclass may + * also want to override the default implementation of + * {@link ReadOnlyNodeTypeManager} for the following methods: + *
    + *
  • {@link #getValueFactory()}
  • + *
  • {@link #getNameMapper()}
  • + *
+ */ +public abstract class ReadWriteNodeTypeManager extends ReadOnlyNodeTypeManager { + + /** + * Called by the methods {@link #registerNodeType(NodeTypeDefinition,boolean)}, + * {@link #registerNodeTypes(NodeTypeDefinition[], boolean)}, + * {@link #unregisterNodeType(String)} and {@link #unregisterNodeTypes(String[])} + * to acquire a fresh {@link Root} instance that can be used to persist the + * requested node type changes (and nothing else). + *

+ * This default implementation throws an {@link UnsupportedOperationException}. + * + * @return fresh {@link Root} instance. + */ + @Nonnull + protected Root getWriteRoot() { + throw new UnsupportedOperationException(); + } + + /** + * Called by the {@link ReadWriteNodeTypeManager} implementation methods to + * refresh the state of the session associated with this instance. + * That way the session is kept in sync with the latest global state + * seen by the node type manager. + * + * @throws RepositoryException if the session could not be refreshed + */ + protected void refresh() throws RepositoryException { + } + + /** + * Utility method for registering node types from a CND format. + * @param cnd reader for the CND + * @throws ParseException if parsing the CND fails + * @throws RepositoryException if registering the node types fails + */ + public void registerNodeTypes(Reader cnd) throws ParseException, RepositoryException { + Root root = getWriteRoot(); + + CompactNodeTypeDefReader> reader = + new CompactNodeTypeDefReader>( + cnd, null, new DefBuilderFactory(root.getTree("/"))); + + Map templates = Maps.newHashMap(); + for (NodeTypeTemplate template : reader.getNodeTypeDefinitions()) { + templates.put(template.getName(), template); + } + + for (NodeTypeTemplate template : templates.values()) { + if (!template.isMixin() && !NT_BASE.equals(template.getName())) { + String[] supertypes = + template.getDeclaredSupertypeNames(); + if (supertypes.length == 0) { + template.setDeclaredSuperTypeNames( + new String[] {NT_BASE}); + } else { + // Check whether we need to add the implicit "nt:base" supertype + boolean needsNtBase = true; + for (String name : supertypes) { + NodeTypeDefinition st = templates.get(name); + if (st == null) { + st = getNodeType(name); + } + if (st != null && !st.isMixin()) { + needsNtBase = false; + } + } + if (needsNtBase) { + String[] withBase = new String[supertypes.length + 1]; + withBase[0] = NT_BASE; + System.arraycopy(supertypes, 0, withBase, 1, supertypes.length); + template.setDeclaredSuperTypeNames(withBase); + } + } + } + } + + try { + internalRegister(root, templates.values(), true); + root.commit(); + refresh(); + } catch (CommitFailedException e) { + throw new RepositoryException(e); + } + } + + //----------------------------------------------------< NodeTypeManager >--- + + @Override + public NodeType registerNodeType(NodeTypeDefinition ntd, boolean allowUpdate) throws RepositoryException { + // TODO proper node type registration... (OAK-66, OAK-411) + Root root = getWriteRoot(); + Tree types = getOrCreateNodeTypes(root); + try { + NodeType type = internalRegister(types, ntd, allowUpdate); + root.commit(); + refresh(); + return type; + } catch (CommitFailedException e) { + throw new RepositoryException(e); + } + } + + @Override + public final NodeTypeIterator registerNodeTypes(NodeTypeDefinition[] ntds, boolean allowUpdate) + throws RepositoryException { + // TODO handle inter-type dependencies (OAK-66, OAK-411) + Root root = getWriteRoot(); + try { + List list = internalRegister( + root, Arrays.asList(ntds), allowUpdate); + root.commit(); + refresh(); + return new NodeTypeIteratorAdapter(list); + } catch (CommitFailedException e) { + throw new RepositoryException(e); + } + } + + private List internalRegister( + Root root, Iterable ntds, + boolean allowUpdate) throws RepositoryException { + Tree types = getOrCreateNodeTypes(root); + List list = Lists.newArrayList(); + for (NodeTypeDefinition ntd : ntds) { + list.add(internalRegister(types, ntd, allowUpdate)); + } + return list; + } + + private NodeType internalRegister( + Tree types, NodeTypeDefinition ntd, boolean allowUpdate) + throws RepositoryException { + String jcrName = ntd.getName(); + String oakName = getOakName(jcrName); + + Tree type = types.getChild(oakName); + if (type != null) { + if (allowUpdate) { + type.remove(); + } else { + throw new NodeTypeExistsException( + "Node type " + jcrName + " already exists"); + } + } + type = types.addChild(oakName); + + NodeUtil node = new NodeUtil(type, getNameMapper()); + node.setName(JCR_PRIMARYTYPE, NT_NODETYPE); + node.setName(JCR_NODETYPENAME, jcrName); + node.setNames(JCR_SUPERTYPES, ntd.getDeclaredSupertypeNames()); + node.setBoolean(JCR_IS_ABSTRACT, ntd.isAbstract()); + node.setBoolean(JCR_IS_QUERYABLE, ntd.isQueryable()); + node.setBoolean(JCR_ISMIXIN, ntd.isMixin()); + + // TODO fail if not orderable but a supertype is orderable. See 3.7.6.7 Node Type Attribute Subtyping Rules (OAK-411) + node.setBoolean(JCR_HASORDERABLECHILDNODES, ntd.hasOrderableChildNodes()); + String primaryItemName = ntd.getPrimaryItemName(); + + // TODO fail if a supertype specifies a different primary item. See 3.7.6.7 Node Type Attribute Subtyping Rules (OAK-411) + if (primaryItemName != null) { + node.setName(JCR_PRIMARYITEMNAME, primaryItemName); + } + + // TODO fail on invalid item definitions. See 3.7.6.8 Item Definitions in Subtypes (OAK-411) + PropertyDefinition[] propertyDefinitions = ntd.getDeclaredPropertyDefinitions(); + if (propertyDefinitions != null) { + int pdn = 1; + for (PropertyDefinition pd : propertyDefinitions) { + NodeUtil def = node.addChild(JCR_PROPERTYDEFINITION + pdn++, NT_PROPERTYDEFINITION); + internalRegisterPropertyDefinition(def, pd); + } + } + + NodeDefinition[] nodeDefinitions = ntd.getDeclaredChildNodeDefinitions(); + if (nodeDefinitions != null) { + int ndn = 1; + for (NodeDefinition nd : nodeDefinitions) { + NodeUtil def = node.addChild(JCR_CHILDNODEDEFINITION + ndn++, NT_CHILDNODEDEFINITION); + internalRegisterNodeDefinition(def, nd); + } + } + + return new NodeTypeImpl(this, getValueFactory(), node); + } + + private static void internalRegisterItemDefinition( + NodeUtil node, ItemDefinition def) { + String name = def.getName(); + if (!"*".equals(name)) { + node.setName(JCR_NAME, name); + } + + // TODO avoid unbounded recursive auto creation. See 3.7.2.3.5 Chained Auto-creation (OAK-411) + node.setBoolean(JCR_AUTOCREATED, def.isAutoCreated()); + node.setBoolean(JCR_MANDATORY, def.isMandatory()); + node.setBoolean(JCR_PROTECTED, def.isProtected()); + node.setString( + JCR_ONPARENTVERSION, + OnParentVersionAction.nameFromValue(def.getOnParentVersion())); + } + + private static void internalRegisterPropertyDefinition( + NodeUtil node, PropertyDefinition def) { + internalRegisterItemDefinition(node, def); + + node.setString( + JCR_REQUIREDTYPE, + PropertyType.nameFromValue(def.getRequiredType()).toUpperCase()); + node.setBoolean(JCR_MULTIPLE, def.isMultiple()); + node.setBoolean(JCR_IS_FULLTEXT_SEARCHABLE, def.isFullTextSearchable()); + node.setBoolean(JCR_IS_QUERY_ORDERABLE, def.isQueryOrderable()); + node.setStrings(JCR_AVAILABLE_QUERY_OPERATORS, def.getAvailableQueryOperators()); + + String[] constraints = def.getValueConstraints(); + if (constraints != null) { + node.setStrings(JCR_VALUECONSTRAINTS, constraints); + } + + Value[] values = def.getDefaultValues(); + if (values != null) { + node.setValues(JCR_DEFAULTVALUES, values); + } + } + + private static void internalRegisterNodeDefinition(NodeUtil node, NodeDefinition def) { + internalRegisterItemDefinition(node, def); + + node.setBoolean(JCR_SAMENAMESIBLINGS, def.allowsSameNameSiblings()); + node.setNames( + JCR_REQUIREDPRIMARYTYPES, + def.getRequiredPrimaryTypeNames()); + String defaultPrimaryType = def.getDefaultPrimaryTypeName(); + if (defaultPrimaryType != null) { + node.setName(JCR_DEFAULTPRIMARYTYPE, defaultPrimaryType); + } + } + + private static Tree getOrCreateNodeTypes(Root root) { + Tree types = root.getTree(NODE_TYPES_PATH); + if (types == null) { + Tree system = root.getTree('/' + JCR_SYSTEM); + if (system == null) { + system = root.getTree("/").addChild(JCR_SYSTEM); + } + types = system.addChild(JCR_NODE_TYPES); + } + return types; + } + + @Override + public void unregisterNodeType(String name) throws RepositoryException { + Tree type = null; + Root root = getWriteRoot(); + Tree types = root.getTree(NODE_TYPES_PATH); + if (types != null) { + type = types.getChild(getOakName(name)); + } + if (type == null) { + throw new NoSuchNodeTypeException("Node type " + name + " can not be unregistered."); + } + + try { + type.remove(); + root.commit(); + refresh(); + } catch (CommitFailedException e) { + throw new RepositoryException("Failed to unregister node type " + name, e); + } + } + + @Override + public void unregisterNodeTypes(String[] names) throws RepositoryException { + Root root = getWriteRoot(); + Tree types = root.getTree(NODE_TYPES_PATH); + if (types == null) { + throw new NoSuchNodeTypeException("Node types can not be unregistered."); + } + + try { + for (String name : names) { + Tree type = types.getChild(getOakName(name)); + if (type == null) { + throw new NoSuchNodeTypeException("Node type " + name + " can not be unregistered."); + } + type.remove(); + } + root.commit(); + refresh(); + } catch (CommitFailedException e) { + throw new RepositoryException("Failed to unregister node types", e); + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/RegistrationValidator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/RegistrationValidator.java new file mode 100644 index 00000000000..ab838477429 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/RegistrationValidator.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.plugins.nodetype; + +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.core.ReadOnlyTree; +import org.apache.jackrabbit.oak.plugins.name.NamespaceConstants; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.util.NodeUtil; +import org.apache.jackrabbit.util.Text; + +/** + * Validator implementation that is responsible for validating any modification + * made to node type definitions. This includes: + * + *

    + *
  • validate new definitions
  • + *
  • detect collisions,
  • + *
  • prevent circular inheritance,
  • + *
  • reject modifications to definitions that render existing content invalid,
  • + *
  • prevent un-registration of built-in node types.
  • + *
+ */ +class RegistrationValidator implements Validator { + + private final ReadOnlyNodeTypeManager beforeMgr; + private final ReadOnlyNodeTypeManager afterMgr; + + private final ReadOnlyTree parentBefore; + private final ReadOnlyTree parentAfter; + + RegistrationValidator(ReadOnlyNodeTypeManager beforeMgr, ReadOnlyNodeTypeManager afterMgr, + ReadOnlyTree parentBefore, ReadOnlyTree parentAfter) { + this.beforeMgr = beforeMgr; + this.afterMgr = afterMgr; + this.parentBefore = parentBefore; + this.parentAfter = parentAfter; + } + + //----------------------------------------------------------< Validator >--- + @Override + public void propertyAdded(PropertyState after) throws CommitFailedException { + // TODO + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) throws CommitFailedException { + // TODO + } + + @Override + public void propertyDeleted(PropertyState before) throws CommitFailedException { + // TODO + } + + @Override + public Validator childNodeAdded(String name, NodeState after) throws CommitFailedException { + // TODO + return null; + } + + @Override + public Validator childNodeChanged(String name, NodeState before, NodeState after) throws CommitFailedException { + // TODO + return null; + } + + @Override + public Validator childNodeDeleted(String name, NodeState before) throws CommitFailedException { + NodeUtil nodeBefore = new NodeUtil(new ReadOnlyTree(parentBefore, name, before)); + if (nodeBefore.hasPrimaryNodeTypeName(JcrConstants.NT_NODETYPE)) { + if (isBuiltInNodeType(name)) { + throw new CommitFailedException(new ConstraintViolationException("Attempt to unregister a built-in node type")); + } + } + // TODO + return null; + } + + //------------------------------------------------------------< private >--- + + private static boolean isBuiltInNodeType(String name) { + // cheap way to determine if a given node type should be considered built-in + String prefix = Text.getNamespacePrefix(name); + return NamespaceConstants.RESERVED_PREFIXES.contains(prefix); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/RegistrationValidatorProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/RegistrationValidatorProvider.java new file mode 100644 index 00000000000..056f32920fd --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/RegistrationValidatorProvider.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.oak.core.ReadOnlyTree; +import org.apache.jackrabbit.oak.spi.commit.SubtreeValidator; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * ValidationProvider implementation that returns a {@code SubtreeValidator} + * that is looking for changes made to /jcr:system/jcr:nodeTypes and + * is responsible for making sure that any modifications made to node type + * definitions are valid. + */ +public class RegistrationValidatorProvider implements ValidatorProvider { + + @Nonnull + @Override + public Validator getRootValidator(NodeState before, NodeState after) { + Validator validator = new RegistrationValidator(new ValidatingNodeTypeManager(before), + new ValidatingNodeTypeManager(after), + new ReadOnlyTree(before), new ReadOnlyTree(after)); + return new SubtreeValidator(validator, JcrConstants.JCR_SYSTEM, NodeTypeConstants.JCR_NODE_TYPES); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/RootNodeDefinition.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/RootNodeDefinition.java new file mode 100644 index 00000000000..19062fc9e0f --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/RootNodeDefinition.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype; + +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.version.OnParentVersionAction; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Node definition for the root node. + */ +final class RootNodeDefinition implements NodeDefinition { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(RootNodeDefinition.class); + + private static final String REP_ROOT = "rep:root"; + + private final NodeTypeManager ntManager; + + RootNodeDefinition(NodeTypeManager ntManager) { + this.ntManager = ntManager; + } + + //-----------------------------------------------------< NodeDefinition >--- + @Override + public NodeType[] getRequiredPrimaryTypes() { + try { + return new NodeType[] {ntManager.getNodeType(REP_ROOT)}; + } catch (RepositoryException e) { + return new NodeType[0]; + } + } + + @Override + public String[] getRequiredPrimaryTypeNames() { + return new String[] {REP_ROOT}; + } + + @Override + public NodeType getDefaultPrimaryType() { + try { + return ntManager.getNodeType(REP_ROOT); + } catch (RepositoryException e) { + return null; + } + } + + @Override + public String getDefaultPrimaryTypeName() { + return REP_ROOT; + } + + @Override + public boolean allowsSameNameSiblings() { + return false; + } + + @Override + public NodeType getDeclaringNodeType() { + try { + return ntManager.getNodeType(REP_ROOT); + } catch (RepositoryException e) { + return null; + } + } + + @Override + public String getName() { + return REP_ROOT; + } + + @Override + public boolean isAutoCreated() { + return true; + } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public int getOnParentVersion() { + return OnParentVersionAction.VERSION; + } + + @Override + public boolean isProtected() { + return false; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/TypeValidator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/TypeValidator.java new file mode 100644 index 00000000000..b61d94ad719 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/TypeValidator.java @@ -0,0 +1,307 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype; + +import java.util.List; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.core.ReadOnlyTree; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.plugins.value.ValueFactoryImpl; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStateUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES; +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; +import static org.apache.jackrabbit.oak.api.Type.STRING; +import static org.apache.jackrabbit.oak.api.Type.STRINGS; + +/** + * Validator implementation that check JCR node type constraints. + * + * TODO: check protected properties and the structure they enforce. some of + * those checks may have to go into separate validator classes. This class + * should only perform checks based on node type information. E.g. it + * cannot and should not check whether the value of the protected jcr:uuid + * is unique. + */ +class TypeValidator implements Validator { + private static final Logger log = LoggerFactory.getLogger(TypeValidator.class); + + private final ReadOnlyNodeTypeManager ntm; + private final ReadOnlyTree parent; + private final NamePathMapper mapper; + + private EffectiveNodeType parentType; + + @Nonnull + private EffectiveNodeType getParentType() throws RepositoryException { + if (parentType == null) { + parentType = getEffectiveNodeType(parent); + } + return parentType; + } + + public TypeValidator(ReadOnlyNodeTypeManager ntm, ReadOnlyTree parent, NamePathMapper mapper) { + this.ntm = ntm; + this.parent = parent; + this.mapper = mapper; + } + + //----------------------------------------------------------< Validator >--- + + @Override + public void propertyAdded(PropertyState after) throws CommitFailedException { + if (isHidden(after)) { + return; + } + try { + checkPrimaryAndMixinTypes(after); + getParentType().checkSetProperty(after); + } catch (RepositoryException e) { + throw new CommitFailedException("Cannot add property '" + after.getName() + "' at " + parent.getPath(), e); + } catch (IllegalStateException e) { + throw new CommitFailedException("Cannot add property '" + after.getName() + "' at " + parent.getPath(), e); + } + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) throws CommitFailedException { + if (isHidden(after)) { + return; + } + try { + checkPrimaryAndMixinTypes(after); + getParentType().checkSetProperty(after); + } catch (RepositoryException e) { + throw new CommitFailedException("Cannot set property '" + after.getName() + "' at " + parent.getPath(), e); + } catch (IllegalStateException e) { + throw new CommitFailedException("Cannot set property '" + after.getName() + "' at " + parent.getPath(), e); + } + } + + @Override + public void propertyDeleted(PropertyState before) throws CommitFailedException { + if (isHidden(before)) { + return; + } + try { + getParentType().checkRemoveProperty(before); + } catch (RepositoryException e) { + throw new CommitFailedException("Cannot remove property '" + before.getName() + "' at " + parent.getPath(), e); + } catch (IllegalStateException e) { + throw new CommitFailedException("Cannot remove property '" + before.getName() + "' at " + parent.getPath(), e); + } + } + + @Override + public Validator childNodeAdded(String name, NodeState after) throws CommitFailedException { + try { + getParentType().checkAddChildNode(name, getNodeType(after)); + + ReadOnlyTree addedTree = new ReadOnlyTree(parent, name, after); + EffectiveNodeType addedType = getEffectiveNodeType(addedTree); + addedType.checkMandatoryItems(addedTree); + return new TypeValidator(ntm, new ReadOnlyTree(parent, name, after), mapper); + } catch (RepositoryException e) { + throw new CommitFailedException("Cannot add node '" + name + "' at " + parent.getPath(), e); + } catch (IllegalStateException e) { + throw new CommitFailedException("Cannot add node '" + name + "' at " + parent.getPath(), e); + } + } + + @Override + public Validator childNodeChanged(String name, NodeState before, NodeState after) throws CommitFailedException { + return new TypeValidator(ntm, new ReadOnlyTree(parent, name, after), mapper); + } + + @Override + public Validator childNodeDeleted(String name, NodeState before) throws CommitFailedException { + try { + getParentType().checkRemoveNode(name, getNodeType(before)); + return null; + } catch (RepositoryException e) { + throw new CommitFailedException("Cannot remove node '" + name + "' at " + parent.getPath(), e); + } catch (IllegalStateException e) { + throw new CommitFailedException("Cannot add node '" + name + "' at " + parent.getPath(), e); + } + } + + //------------------------------------------------------------< private >--- + + private void checkPrimaryAndMixinTypes(PropertyState after) throws RepositoryException { + boolean primaryType = JCR_PRIMARYTYPE.equals(after.getName()); + boolean mixinType = JCR_MIXINTYPES.equals(after.getName()); + if (primaryType || mixinType) { + for (String ntName : after.getValue(STRINGS)) { + NodeType nt = ntm.getNodeType(ntName); + if (nt.isAbstract()) { + throw new ConstraintViolationException("Can't create node with abstract type: " + ntName); + } + if (primaryType && nt.isMixin()) { + throw new ConstraintViolationException("Can't assign mixin for primary type: " + ntName); + } + if (mixinType && !nt.isMixin()) { + throw new ConstraintViolationException("Can't assign primary type for mixin: " + ntName); + } + } + } + } + + @CheckForNull + private NodeType getNodeType(NodeState state) throws RepositoryException { + PropertyState type = state.getProperty(JCR_PRIMARYTYPE); + if (type == null || type.count() == 0) { + // TODO: review again + return null; + } else { + String ntName = type.getValue(STRING, 0); + return ntm.getNodeType(ntName); + } + } + + private static boolean isHidden(PropertyState state) { + return NodeStateUtils.isHidden(state.getName()); + } + + // FIXME: the same is also required on JCR level. probably keeping that in 1 single location would be preferable. + private EffectiveNodeType getEffectiveNodeType(Tree tree) throws RepositoryException { + return new EffectiveNodeType(ntm.getEffectiveNodeTypes(tree)); + } + + private class EffectiveNodeType { + private final Iterable allTypes; + + public EffectiveNodeType(Iterable allTypes) { + this.allTypes = allTypes; + } + + public void checkSetProperty(PropertyState property) throws RepositoryException { + PropertyDefinition definition = getDefinition(property); + if (definition.isProtected()) { + return; + } + + NodeType nt = definition.getDeclaringNodeType(); + if (definition.isMultiple()) { + List values = ValueFactoryImpl.createValues(property, mapper); + if (!nt.canSetProperty(property.getName(), values.toArray(new Value[values.size()]))) { + throw new ConstraintViolationException("Cannot set property '" + property.getName() + "' to '" + values + '\''); + } + } else { + Value v = ValueFactoryImpl.createValue(property, mapper); + if (!nt.canSetProperty(property.getName(), v)) { + throw new ConstraintViolationException("Cannot set property '" + property.getName() + "' to '" + v + '\''); + } + } + } + + public void checkRemoveProperty(PropertyState property) throws RepositoryException { + PropertyDefinition definition = getDefinition(property); + if (definition.isProtected()) { + return; + } + + if (!definition.getDeclaringNodeType().canRemoveProperty(property.getName())) { + throw new ConstraintViolationException("Cannot remove property '" + property.getName() + '\''); + } + } + + public void checkRemoveNode(String name, NodeType nodeType) throws RepositoryException { + NodeDefinition definition = getDefinition(name, nodeType); + if (definition.isProtected()) { + return; + } + + if (!definition.getDeclaringNodeType().canRemoveNode(name)) { + throw new ConstraintViolationException("Cannot remove node '" + name + '\''); + } + } + + public void checkAddChildNode(String name, NodeType nodeType) throws RepositoryException { + NodeDefinition definition = getDefinition(name, nodeType); + if (definition.isProtected()) { + return; + } + + if (nodeType == null) { + if (!definition.getDeclaringNodeType().canAddChildNode(name)) { + throw new ConstraintViolationException("Cannot add node '" + name + '\''); + } + } else { + if (!definition.getDeclaringNodeType().canAddChildNode(name, nodeType.getName())) { + throw new ConstraintViolationException("Cannot add node '" + name + "' of type '" + nodeType.getName() + '\''); + } + } + + } + + public void checkMandatoryItems(ReadOnlyTree tree) throws ConstraintViolationException { + for (NodeType nodeType : allTypes) { + for (PropertyDefinition pd : nodeType.getPropertyDefinitions()) { + String name = pd.getName(); + if (pd.isMandatory() && !pd.isProtected() && tree.getProperty(name) == null) { + throw new ConstraintViolationException( + "Property '" + name + "' in '" + nodeType.getName() + "' is mandatory"); + } + } + for (NodeDefinition nd : nodeType.getChildNodeDefinitions()) { + String name = nd.getName(); + if (nd.isMandatory() && !nd.isProtected() && tree.getChild(name) == null) { + throw new ConstraintViolationException( + "Node '" + name + "' in '" + nodeType.getName() + "' is mandatory"); + } + } + } + } + + private PropertyDefinition getDefinition(PropertyState property) throws RepositoryException { + String propertyName = property.getName(); + int propertyType = property.getType().tag(); + boolean isMultiple = property.isArray(); + + return ntm.getDefinition(allTypes, propertyName, isMultiple, propertyType, true); + } + + private NodeDefinition getDefinition(String nodeName, NodeType nodeType) throws RepositoryException { + // FIXME: ugly hack to workaround sns-hack that was used to map sns-item definitions with node types. + String nameToCheck = nodeName; + if (nodeName.startsWith("jcr:childNodeDefinition") && !nodeName.equals("jcr:childNodeDefinition")) { + nameToCheck = nodeName.substring(0, "jcr:childNodeDefinition".length()); + } + if (nodeName.startsWith("jcr:propertyDefinition") && !nodeName.equals("jcr:propertyDefinition")) { + nameToCheck = nodeName.substring(0, "jcr:propertyDefinition".length()); + } + return ntm.getDefinition(allTypes, nameToCheck, nodeType); + } + + + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/TypeValidatorProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/TypeValidatorProvider.java new file mode 100644 index 00000000000..34e8e30b9ef --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/TypeValidatorProvider.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.plugins.nodetype; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.apache.jackrabbit.oak.core.ReadOnlyTree; +import org.apache.jackrabbit.oak.namepath.NameMapperImpl; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.namepath.NamePathMapperImpl; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +@Component +@Service(ValidatorProvider.class) +public class TypeValidatorProvider implements ValidatorProvider { + + @Override + public Validator getRootValidator(NodeState before, final NodeState after) { + ReadOnlyNodeTypeManager ntm = new ValidatingNodeTypeManager(after); + ReadOnlyTree root = new ReadOnlyTree(after); + final NamePathMapper mapper = new NamePathMapperImpl(new NameMapperImpl(root)); + return new TypeValidator(ntm, root, mapper); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/ValidatingNodeTypeManager.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/ValidatingNodeTypeManager.java new file mode 100644 index 00000000000..1e35a8cb9aa --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/ValidatingNodeTypeManager.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype; + +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.core.ReadOnlyTree; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.NODE_TYPES_PATH; + +/** + * NodeTypeManager implementation based on a given {@code NodeState} in order + * to be used for the various node type related validators. + */ +class ValidatingNodeTypeManager extends ReadWriteNodeTypeManager { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(ValidatingNodeTypeManager.class); + + private final Tree types; + + ValidatingNodeTypeManager(NodeState nodeState) { + this.types = getTypes(nodeState); + } + + @Override + protected Tree getTypes() { + return types; + } + + private Tree getTypes(NodeState after) { + Tree tree = new ReadOnlyTree(after); + for (String name : PathUtils.elements(NODE_TYPES_PATH)) { + if (tree == null) { + break; + } else { + tree = tree.getChild(name); + } + } + return tree; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/BinaryConstraint.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/BinaryConstraint.java new file mode 100644 index 00000000000..f2869bab60a --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/BinaryConstraint.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype.constraint; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +public class BinaryConstraint extends LongConstraint { + public BinaryConstraint(String definition) { + super(definition); + } + + @Override + protected Long getValue(Value value) throws RepositoryException { + return value.getBinary().getSize(); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/BooleanConstraint.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/BooleanConstraint.java new file mode 100644 index 00000000000..8e69314f26a --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/BooleanConstraint.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.plugins.nodetype.constraint; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import com.google.common.base.Predicate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BooleanConstraint implements Predicate { + private static final Logger log = LoggerFactory.getLogger(BooleanConstraint.class); + + private final Boolean requiredValue; + + public BooleanConstraint(String definition) { + if ("true".equals(definition)) { + requiredValue = true; + } + else if ("false".equals(definition)) { + requiredValue = false; + } + else { + requiredValue = null; + log.warn('\'' + definition + "' is not a valid value constraint format for boolean values"); + } + } + + @Override + public boolean apply(Value value) { + try { + return value != null && requiredValue != null && value.getBoolean() == requiredValue; + } + catch (RepositoryException e) { + log.warn("Error checking boolean constraint " + this, e); + return false; + } + } + + @Override + public String toString() { + return "'" + requiredValue + '\''; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/Constraints.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/Constraints.java new file mode 100644 index 00000000000..5073a2eda32 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/Constraints.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.plugins.nodetype.constraint; + +import javax.jcr.PropertyType; +import javax.jcr.Value; + +import com.google.common.base.Predicate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Constraints { + private static final Logger log = LoggerFactory.getLogger(Constraints.class); + + private Constraints() { + } + + public static Predicate valueConstraint(int type, String constraint) { + switch (type) { + case PropertyType.STRING: + return new StringConstraint(constraint); + case PropertyType.BINARY: + return new BinaryConstraint(constraint); + case PropertyType.LONG: + return new LongConstraint(constraint); + case PropertyType.DOUBLE: + return new DoubleConstraint(constraint); + case PropertyType.DATE: + return new DateConstraint(constraint); + case PropertyType.BOOLEAN: + return new BooleanConstraint(constraint); + case PropertyType.NAME: + return new NameConstraint(constraint); + case PropertyType.PATH: + return new PathConstraint(constraint); + case PropertyType.REFERENCE: + return new ReferenceConstraint(constraint); + case PropertyType.WEAKREFERENCE: + return new ReferenceConstraint(constraint); + case PropertyType.URI: + return new StringConstraint(constraint); + case PropertyType.DECIMAL: + return new DecimalConstraint(constraint); + default: + String msg = "Invalid property type: " + type; + log.warn(msg); + throw new IllegalArgumentException(msg); + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/DateConstraint.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/DateConstraint.java new file mode 100644 index 00000000000..447b5eb02a4 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/DateConstraint.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.plugins.nodetype.constraint; + +import java.util.Calendar; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.jackrabbit.value.DateValue; + +public class DateConstraint extends NumericConstraint { + public DateConstraint(String definition) { + super(definition); + } + + @Override + protected Calendar getBound(String bound) { + try { + return DateValue.valueOf(bound).getDate(); + } + catch (RepositoryException e) { + throw (NumberFormatException) new NumberFormatException().initCause(e); + } + } + + @Override + protected Calendar getValue(Value value) throws RepositoryException { + return value.getDate(); + } + + @Override + protected boolean less(Calendar val, Calendar bound) { + return val.getTimeInMillis() < bound.getTimeInMillis(); + } + + @Override + protected boolean equals(Calendar val, Calendar bound) { + return val.getTimeInMillis() == bound.getTimeInMillis(); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/DecimalConstraint.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/DecimalConstraint.java new file mode 100644 index 00000000000..83537ef7ac9 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/DecimalConstraint.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype.constraint; + +import java.math.BigDecimal; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +public class DecimalConstraint extends NumericConstraint { + public DecimalConstraint(String definition) { + super(definition); + } + + @Override + protected BigDecimal getBound(String bound) { + return bound == null || bound.isEmpty() + ? null + : new BigDecimal(bound); + } + + @Override + protected BigDecimal getValue(Value value) throws RepositoryException { + return value.getDecimal(); + } + + @Override + protected boolean less(BigDecimal val, BigDecimal bound) { + return val.compareTo(bound) < 0; + } + + @Override + protected boolean equals(BigDecimal val, BigDecimal bound) { + return val.compareTo(bound) == 0; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/DoubleConstraint.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/DoubleConstraint.java new file mode 100644 index 00000000000..808c03340f6 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/DoubleConstraint.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype.constraint; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DoubleConstraint extends NumericConstraint { + private static final Logger log = LoggerFactory.getLogger(DoubleConstraint.class); + + public DoubleConstraint(String definition) { + super(definition); + } + + @Override + protected Double getBound(String bound) { + return bound == null || bound.isEmpty() + ? null + : Double.parseDouble(bound); + } + + @Override + protected Double getValue(Value value) throws RepositoryException { + return value.getDouble(); + } + + @Override + protected boolean less(Double val, Double bound) { + return val < bound; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/LongConstraint.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/LongConstraint.java new file mode 100644 index 00000000000..653529b37c1 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/LongConstraint.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype.constraint; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LongConstraint extends NumericConstraint { + private static final Logger log = LoggerFactory.getLogger(LongConstraint.class); + + public LongConstraint(String definition) { + super(definition); + } + + @Override + protected Long getBound(String bound) { + return bound == null || bound.isEmpty() + ? null + : Long.parseLong(bound); + } + + @Override + protected Long getValue(Value value) throws RepositoryException { + return value.getLong(); + } + + @Override + protected boolean less(Long val, Long bound) { + return val < bound; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/NameConstraint.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/NameConstraint.java new file mode 100644 index 00000000000..d6372142dd1 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/NameConstraint.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.plugins.nodetype.constraint; + +import javax.annotation.Nullable; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import com.google.common.base.Predicate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NameConstraint implements Predicate { + private static final Logger log = LoggerFactory.getLogger(NameConstraint.class); + + private final String requiredValue; + + public NameConstraint(String definition) { + requiredValue = definition; + } + + @Override + public boolean apply(@Nullable Value value) { + try { + return value != null && requiredValue != null && requiredValue.equals(value.getString()); + } + catch (RepositoryException e) { + log.warn("Error checking name constraint " + this, e); + return false; + } + } + + @Override + public String toString() { + return '\'' + requiredValue + '\''; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/NumericConstraint.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/NumericConstraint.java new file mode 100644 index 00000000000..2fd6852c76f --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/NumericConstraint.java @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype.constraint; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.annotation.Nullable; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import com.google.common.base.Predicate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class NumericConstraint implements Predicate { + private static final Logger log = LoggerFactory.getLogger(NumericConstraint.class); + + private boolean invalid; + private boolean lowerInclusive; + private T lowerBound; + private T upperBound; + private boolean upperInclusive; + + protected NumericConstraint(String definition) { + // format: '(, )', '[, ]', '(, )' etc. + Pattern pattern = Pattern.compile("([\\(\\[])([^,]*),([^\\)\\]]*)([\\)\\]])"); + Matcher matcher = pattern.matcher(definition); + if (matcher.matches()) { + try { + // group 1 is lower bound inclusive/exclusive + lowerInclusive = "[".equals(matcher.group(1)); + + // group 2 is lower, group 3 is upper bound + lowerBound = getBound(matcher.group(2)); + upperBound = getBound(matcher.group(3)); + + // group 4 is upper bound inclusive/exclusive + upperInclusive = "]".equals(matcher.group(4)); + } + catch (NumberFormatException e) { + invalid(definition); + } + } + else { + invalid(definition); + } + } + + private void invalid(String definition) { + invalid = true; + log.warn('\'' + definition + "' is not a valid value constraint format for numeric values"); + } + + protected abstract T getBound(String bound); + + @Override + public boolean apply(@Nullable Value value) { + if (value == null || invalid) { + return false; + } + + try { + T t = getValue(value); + if (lowerBound != null) { + if (lowerInclusive) { + if (less(t, lowerBound)) { + return false; + } + } else { + if (lessOrEqual(t, lowerBound)) { + return false; + } + } + } + if (upperBound != null) { + if (upperInclusive) { + if (greater(t, upperBound)) { + return false; + } + } else { + if (greaterOrEqual(t, upperBound)) { + return false; + } + } + } + return true; + } + catch (RepositoryException e) { + log.warn("Error checking numeric constraint " + this, e); + return false; + } + } + + protected abstract T getValue(Value value) throws RepositoryException; + protected abstract boolean less(T val, T bound); + + protected boolean greater(T val, T bound) { + return less(bound, val); + } + + protected boolean equals(T val, T bound) { + return val.equals(bound); + } + + protected boolean greaterOrEqual(T val, T bound) { + return greater(val, bound) || equals(val, bound); + } + + protected boolean lessOrEqual(T val, T bound) { + return less(val, bound) || equals(val, bound); + } + + @Override + public String toString() { + return (lowerInclusive ? "[" : "(") + + (lowerBound == null ? "" : lowerBound) + ", " + + (upperBound == null ? "" : upperBound) + + (upperInclusive ? "]" : ")"); + }} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/PathConstraint.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/PathConstraint.java new file mode 100644 index 00000000000..dea8f58d411 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/PathConstraint.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.plugins.nodetype.constraint; + +import javax.annotation.Nullable; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import com.google.common.base.Predicate; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PathConstraint implements Predicate { + private static final Logger log = LoggerFactory.getLogger(PathConstraint.class); + + private final String requiredValue; + + public PathConstraint(String definition) { + this.requiredValue = definition; + } + + @Override + public boolean apply(@Nullable Value value) { + try { + if (value == null || requiredValue == null) { + return false; + } + + if ("*".equals(requiredValue)) { + return true; + } + + String actual = value.getString(); + String required = requiredValue; + if (required.endsWith("/*")) { + required = required.substring(0, required.length() - 2); + if (PathUtils.isAncestor(required, actual)) { + return true; + } + } + + return required.equals(actual); + } + catch (RepositoryException e) { + log.warn("Error checking path constraint " + this, e); + return false; + } + } + + @Override + public String toString() { + return '\'' + requiredValue + '\''; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/ReferenceConstraint.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/ReferenceConstraint.java new file mode 100644 index 00000000000..42f26ec9b4f --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/ReferenceConstraint.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.nodetype.constraint; + +import javax.jcr.Value; + +import com.google.common.base.Predicate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ReferenceConstraint implements Predicate { + private static final Logger log = LoggerFactory.getLogger(ReferenceConstraint.class); + + private final String requiredTargetType; + + public ReferenceConstraint(String definition) { + requiredTargetType = definition; + } + + @Override + public boolean apply(Value value) { + // TODO implement ReferenceConstraint + return true; + } + + @Override + public String toString() { + return '\'' + requiredTargetType + '\''; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/StringConstraint.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/StringConstraint.java new file mode 100644 index 00000000000..dcf71bf764e --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/constraint/StringConstraint.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.plugins.nodetype.constraint; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import com.google.common.base.Predicate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class StringConstraint implements Predicate { + private static final Logger log = LoggerFactory.getLogger(StringConstraint.class); + + private final Pattern pattern; + + public StringConstraint(String definition) { + Pattern p; + try { + p = Pattern.compile(definition); + } + catch (PatternSyntaxException pse) { + String msg = '\'' + definition + "' is not valid regular expression syntax"; + log.warn(msg); + p = null; + } + pattern = p; + } + + @Override + public boolean apply(Value value) { + if (value == null) { + return false; + } + + try { + Matcher matcher = pattern.matcher(value.getString()); + return matcher.matches(); + } + catch (RepositoryException e) { + log.warn("Error checking string constraint " + this, e); + return false; + } + } + + @Override + public String toString() { + return "'" + pattern + '\''; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/ChangeFilter.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/ChangeFilter.java new file mode 100644 index 00000000000..1630cc7b824 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/ChangeFilter.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.observation; + +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +class ChangeFilter { + private final int eventTypes; + private final String path; + private final boolean deep; + private final String[] uuid; // TODO implement filtering by uuid + private final String[] nodeTypeName; // TODO implement filtering by nodeTypeName + private final boolean noLocal; // TODO implement filtering by noLocal + + public ChangeFilter(int eventTypes, String path, boolean deep, String[] uuid, String[] nodeTypeName, + boolean noLocal) { + this.eventTypes = eventTypes; + this.path = path; + this.deep = deep; + this.uuid = uuid; + this.nodeTypeName = nodeTypeName; + this.noLocal = noLocal; + } + + public boolean include(int eventType) { + return (this.eventTypes & eventType) != 0; + } + + public boolean include(String path) { + boolean equalPaths = this.path.equals(path); + if (!deep && !equalPaths) { + return false; + } + if (deep && !(PathUtils.isAncestor(this.path, path) || equalPaths)) { + return false; + } + return true; + } + + public boolean include(int eventType, String path, NodeState associatedParentNode) { + return include(eventType) && include(path); + } + + public boolean includeChildren(String path) { + return PathUtils.isAncestor(path, this.path) || + path.equals(this.path) || + deep && PathUtils.isAncestor(this.path, path); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/ChangeProcessor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/ChangeProcessor.java new file mode 100644 index 00000000000..0964b0ee9f9 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/ChangeProcessor.java @@ -0,0 +1,281 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.observation; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import javax.jcr.observation.Event; +import javax.jcr.observation.EventListener; + +import com.google.common.base.Function; +import com.google.common.collect.Iterators; +import org.apache.jackrabbit.commons.iterator.EventIteratorAdapter; +import org.apache.jackrabbit.oak.spi.observation.ChangeExtractor; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; +import org.apache.jackrabbit.oak.spi.state.NodeStateUtils; + +class ChangeProcessor implements Runnable { + private final ObservationManagerImpl observationManager; + private final NamePathMapper namePathMapper; + private final ChangeExtractor changeExtractor; + private final EventListener listener; + private final AtomicReference filterRef; + private volatile boolean running; + private volatile boolean stopping; + private ScheduledFuture future; + + public ChangeProcessor(ObservationManagerImpl observationManager, EventListener listener, ChangeFilter filter) { + this.observationManager = observationManager; + this.namePathMapper = observationManager.getNamePathMapper(); + this.changeExtractor = observationManager.getChangeExtractor(); + this.listener = listener; + filterRef = new AtomicReference(filter); + } + + public void setFilter(ChangeFilter filter) { + filterRef.set(filter); + } + + /** + * Stop this change processor if running. After returning from this methods no further + * events will be delivered. + * @throws IllegalStateException if not yet started or stopped already + */ + public synchronized void stop() { + if (future == null) { + throw new IllegalStateException("Change processor not started"); + } + + try { + stopping = true; + future.cancel(true); + while (running) { + wait(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + finally { + future = null; + } + } + + /** + * Start the change processor on the passed {@code executor}. + * @param executor + * @throws IllegalStateException if started already + */ + public synchronized void start(ScheduledExecutorService executor) { + if (future != null) { + throw new IllegalStateException("Change processor started already"); + } + stopping = false; + future = executor.scheduleWithFixedDelay(this, 100, 1000, TimeUnit.MILLISECONDS); + } + + @Override + public void run() { + running = true; + try{ + EventGeneratingNodeStateDiff diff = new EventGeneratingNodeStateDiff(); + changeExtractor.getChanges(diff); + if (!stopping) { + diff.sendEvents(); + } + } finally { + synchronized (this) { + running = false; + notifyAll(); + } + } + } + + //------------------------------------------------------------< private >--- + + private class EventGeneratingNodeStateDiff implements NodeStateDiff { + public static final int PURGE_LIMIT = 8192; + + private final String path; + private final NodeState associatedParentNode; + + private int childNodeCount; + private List> events; + + EventGeneratingNodeStateDiff(String path, List> events, NodeState associatedParentNode) { + this.path = path; + this.associatedParentNode = associatedParentNode; + this.events = events; + } + + public EventGeneratingNodeStateDiff() { + this("/", new ArrayList>(PURGE_LIMIT), null); + } + + public void sendEvents() { + Iterator eventIt = Iterators.concat(events.iterator()); + if (eventIt.hasNext()) { + observationManager.setHasEvents(); + listener.onEvent(new EventIteratorAdapter(eventIt) { + @Override + public boolean hasNext() { + return !stopping && super.hasNext(); + } + }); + events = new ArrayList>(PURGE_LIMIT); + } + } + + private String jcrPath() { + return namePathMapper.getJcrPath(path); + } + + @Override + public void propertyAdded(PropertyState after) { + if (!stopping && filterRef.get().include(Event.PROPERTY_ADDED, jcrPath(), associatedParentNode)) { + Event event = generatePropertyEvent(Event.PROPERTY_ADDED, path, after); + events.add(Iterators.singletonIterator(event)); + } + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) { + if (!stopping && filterRef.get().include(Event.PROPERTY_CHANGED, jcrPath(), associatedParentNode)) { + Event event = generatePropertyEvent(Event.PROPERTY_CHANGED, path, after); + events.add(Iterators.singletonIterator(event)); + } + } + + @Override + public void propertyDeleted(PropertyState before) { + if (!stopping && filterRef.get().include(Event.PROPERTY_REMOVED, jcrPath(), associatedParentNode)) { + Event event = generatePropertyEvent(Event.PROPERTY_REMOVED, path, before); + events.add(Iterators.singletonIterator(event)); + } + } + + @Override + public void childNodeAdded(String name, NodeState after) { + if (NodeStateUtils.isHidden(name)) { + return; + } + if (!stopping && filterRef.get().includeChildren(jcrPath())) { + Iterator events = generateNodeEvents(Event.NODE_ADDED, path, name, after); + this.events.add(events); + if (++childNodeCount > PURGE_LIMIT) { + sendEvents(); + } + } + } + + @Override + public void childNodeDeleted(String name, NodeState before) { + if (NodeStateUtils.isHidden(name)) { + return; + } + if (!stopping && filterRef.get().includeChildren(jcrPath())) { + Iterator events = generateNodeEvents(Event.NODE_REMOVED, path, name, before); + this.events.add(events); + } + } + + @Override + public void childNodeChanged(String name, NodeState before, NodeState after) { + if (NodeStateUtils.isHidden(name)) { + return; + } + if (!stopping && filterRef.get().includeChildren(jcrPath())) { + EventGeneratingNodeStateDiff diff = new EventGeneratingNodeStateDiff( + PathUtils.concat(path, name), events, after); + after.compareAgainstBaseState(before, diff); + if (events.size() > PURGE_LIMIT) { + diff.sendEvents(); + } + } + } + + private Event generatePropertyEvent(int eventType, String parentPath, PropertyState property) { + String jcrPath = namePathMapper.getJcrPath(PathUtils.concat(parentPath, property.getName())); + + // TODO support userId, identifier, info, date + return new EventImpl(eventType, jcrPath, null, null, null, 0); + } + + private Iterator generateNodeEvents(int eventType, String parentPath, String name, NodeState node) { + ChangeFilter filter = filterRef.get(); + final String path = PathUtils.concat(parentPath, name); + String jcrParentPath = namePathMapper.getJcrPath(parentPath); + String jcrPath = namePathMapper.getJcrPath(path); + + Iterator nodeEvent; + if (filter.include(eventType, jcrParentPath, associatedParentNode)) { + // TODO support userId, identifier, info, date + Event event = new EventImpl(eventType, jcrPath, null, null, null, 0); + nodeEvent = Iterators.singletonIterator(event); + } else { + nodeEvent = Iterators.emptyIterator(); + } + + final int propertyEventType = eventType == Event.NODE_ADDED + ? Event.PROPERTY_ADDED + : Event.PROPERTY_REMOVED; + + Iterator propertyEvents; + if (filter.include(propertyEventType, jcrPath, associatedParentNode)) { + propertyEvents = Iterators.transform( + node.getProperties().iterator(), + new Function() { + @Override + public Event apply(PropertyState property) { + return generatePropertyEvent(propertyEventType, path, property); + } + }); + } else { + propertyEvents = Iterators.emptyIterator(); + } + + Iterator childNodeEvents = filter.includeChildren(jcrPath) + ? Iterators.concat(generateChildEvents(eventType, path, node)) + : Iterators.emptyIterator(); + + return Iterators.concat(nodeEvent, propertyEvents, childNodeEvents); + } + + private Iterator> generateChildEvents(final int eventType, final String parentPath, NodeState node) { + return Iterators.transform( + node.getChildNodeEntries().iterator(), + new Function>() { + @Override + public Iterator apply(ChildNodeEntry entry) { + return generateNodeEvents(eventType, parentPath, entry.getName(), entry.getNodeState()); + } + }); + } + + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/EventImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/EventImpl.java new file mode 100644 index 00000000000..eeee5682a41 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/EventImpl.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.observation; + +import java.util.Collections; +import java.util.Map; + +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.observation.Event; + +public class EventImpl implements Event { + private final int type; + private final String path; + private final String userID; + private final String identifier; + private final Map info; + private final long date; + + public EventImpl(int type, String path, String userID, String identifier, Map info, long date) { + this.type = type; + this.path = path; + this.userID = userID; + this.identifier = identifier; + this.info = info == null ? Collections.emptyMap() : info; + this.date = date; + } + + @Override + public int getType() { + return type; + } + + @Override + public String getPath() throws RepositoryException { + return path; + } + + @Override + public String getUserID() { + return userID; + } + + @Override + public String getIdentifier() throws RepositoryException { + return identifier; + } + + @Override + public Map getInfo() throws RepositoryException { + return info; + } + + @Override + public String getUserData() throws RepositoryException { + throw new UnsupportedRepositoryOperationException("User data not supported"); + } + + @Override + public long getDate() throws RepositoryException { + return date; + } + + @Override + public final boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + + EventImpl that = (EventImpl) other; + return date == that.date && type == that.type && + (identifier == null ? that.identifier == null : identifier.equals(that.identifier)) && + (info == null ? that.info == null : info.equals(that.info)) && + (path == null ? that.path == null : path.equals(that.path)) && + (userID == null ? that.userID == null : userID.equals(that.userID)); + + } + + @Override + public final int hashCode() { + int result = type; + result = 31 * result + (path == null ? 0 : path.hashCode()); + result = 31 * result + (userID == null ? 0 : userID.hashCode()); + result = 31 * result + (identifier == null ? 0 : identifier.hashCode()); + result = 31 * result + (info == null ? 0 : info.hashCode()); + result = 31 * result + (int) (date ^ (date >>> 32)); + return result; + } + + @Override + public String toString() { + return "EventImpl{" + + "type=" + type + + ", path='" + path + '\'' + + ", userID='" + userID + '\'' + + ", identifier='" + identifier + '\'' + + ", info=" + info + + ", date=" + date + + '}'; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/ObservationManagerImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/ObservationManagerImpl.java new file mode 100644 index 00000000000..82974dfe8d2 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/ObservationManagerImpl.java @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.observation; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.observation.EventJournal; +import javax.jcr.observation.EventListener; +import javax.jcr.observation.EventListenerIterator; +import javax.jcr.observation.ObservationManager; + +import com.google.common.base.Preconditions; +import org.apache.jackrabbit.commons.iterator.EventListenerIteratorAdapter; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.core.RootImpl; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.spi.observation.ChangeExtractor; + +public class ObservationManagerImpl implements ObservationManager { + private final RootImpl root; + private final NamePathMapper namePathMapper; + private final ScheduledExecutorService executor; + private final Map processors = new HashMap(); + private final AtomicBoolean hasEvents = new AtomicBoolean(false); + + public ObservationManagerImpl(Root root, NamePathMapper namePathMapper, ScheduledExecutorService executor) { + Preconditions.checkArgument(root instanceof RootImpl, "root must be of actual type RootImpl"); + this.root = ((RootImpl) root); + this.namePathMapper = namePathMapper; + this.executor = executor; + } + + public synchronized void dispose() { + for (ChangeProcessor processor : processors.values()) { + processor.stop(); + } + processors.clear(); + } + + /** + * Determine whether events have been generated since the time this method has been called. + * @return {@code true} if this {@code ObservationManager} instance has generated events + * since the last time this method has been called, {@code false} otherwise. + */ + public boolean hasEvents() { + return hasEvents.getAndSet(false); + } + + @Override + public synchronized void addEventListener(EventListener listener, int eventTypes, String absPath, + boolean isDeep, String[] uuid, String[] nodeTypeName, boolean noLocal) throws RepositoryException { + ChangeFilter filter = new ChangeFilter(eventTypes, absPath, isDeep, uuid, nodeTypeName, noLocal); + ChangeProcessor processor = processors.get(listener); + if (processor == null) { + processor = new ChangeProcessor(this, listener, filter); + processors.put(listener, processor); + processor.start(executor); + } else { + processor.setFilter(filter); + } + } + + @Override + public synchronized void removeEventListener(EventListener listener) { + ChangeProcessor processor = processors.remove(listener); + + if (processor != null) { + processor.stop(); + } + } + + @Override + public EventListenerIterator getRegisteredEventListeners() throws RepositoryException { + return new EventListenerIteratorAdapter(processors.keySet()); + } + + @Override + public void setUserData(String userData) throws RepositoryException { + throw new UnsupportedRepositoryOperationException("User data not supported"); + } + + @Override + public EventJournal getEventJournal() throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + @Override + public EventJournal getEventJournal(int eventTypes, String absPath, boolean isDeep, String[] uuid, String[] + nodeTypeName) throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + //------------------------------------------------------------< internal >--- + + NamePathMapper getNamePathMapper() { + return namePathMapper; + } + + ChangeExtractor getChangeExtractor() { + return root.getChangeExtractor(); + } + + void setHasEvents() { + hasEvents.set(true); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/package-info.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/package-info.java new file mode 100644 index 00000000000..4a51e14ca9c --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Oak plugins. This package contains various oak-core extensions that are + * (still) too small to be placed into their own Maven components. + */ +package org.apache.jackrabbit.oak.plugins; diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/value/BinaryImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/value/BinaryImpl.java new file mode 100644 index 00000000000..8d8e8ead7cc --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/value/BinaryImpl.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.plugins.value; + +import java.io.IOException; +import java.io.InputStream; + +import javax.jcr.Binary; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +/** + * BinaryImpl... + */ +class BinaryImpl implements Binary { + + private final ValueImpl value; + + BinaryImpl(ValueImpl value) { + this.value = value; + } + + ValueImpl getBinaryValue() { + return value.getType() == PropertyType.BINARY ? value : null; + } + + //-------------------------------------------------------------< Binary >--- + + @Override + public InputStream getStream() throws RepositoryException { + return value.getNewStream(); + } + + @Override + public int read(byte[] b, long position) throws IOException, RepositoryException { + InputStream stream = value.getNewStream(); + try { + if (position != stream.skip(position)) { + throw new IOException("Can't skip to position " + position); + } + return stream.read(b); + } finally { + stream.close(); + } + } + + @Override + public long getSize() throws RepositoryException { + switch (value.getType()) { + case PropertyType.NAME: + case PropertyType.PATH: + // need to respect namespace remapping + return value.getString().length(); + default: + return value.getStreamLength(); + } + } + + @Override + public void dispose() { + // nothing to do + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/value/Conversions.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/value/Conversions.java new file mode 100644 index 00000000000..d39a05670d0 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/value/Conversions.java @@ -0,0 +1,352 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.plugins.value; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.Calendar; +import java.util.TimeZone; + +import com.google.common.base.Charsets; +import com.google.common.io.ByteStreams; +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.plugins.memory.StringBasedBlob; +import org.apache.jackrabbit.util.ISO8601; + +/** + * Utility class defining the conversion that take place between {@link org.apache.jackrabbit.oak.api.PropertyState}s + * of different types. All conversions defined in this class are compatible with the conversions specified + * in JSR-283 $3.6.4. However, some conversion in this class might not be defined in JSR-283. + *

+ * Example: + *

+ *    double three = convert("3.0").toDouble();
+ * 
+ */ +public final class Conversions { + + private static final TimeZone UTC = TimeZone.getTimeZone("GMT+00:00"); + + private Conversions() {} + + /** + * A converter converts a value to its representation as a specific target type. Not all target + * types might be supported for a given value in which case implementations throw an exception. + * The default implementations of the various conversion methods all operate on the string + * representation of the underlying value (i.e. call {@code Converter.toString()}. + */ + public abstract static class Converter { + + /** + * Convert to string + * @return string representation of the converted value + */ + public abstract String toString(); + + /** + * Convert to binary. This default implementation returns an new instance + * of {@link StringBasedBlob}. + * @return binary representation of the converted value + */ + public Blob toBinary() { + return new StringBasedBlob(toString()); + } + + /** + * Convert to long. This default implementation is based on {@code Long.parseLong(String)}. + * @return long representation of the converted value + * @throws NumberFormatException + */ + public long toLong() { + return Long.parseLong(toString()); + } + + /** + * Convert to double. This default implementation is based on {@code Double.parseDouble(String)}. + * @return double representation of the converted value + * @throws NumberFormatException + */ + public double toDouble() { + return Double.parseDouble(toString()); + } + + /** + * Convert to date. This default implementation is based on {@code ISO8601.parse(String)}. + * @return date representation of the converted value + * @throws IllegalArgumentException if the string cannot be parsed into a date + */ + public Calendar toCalendar() { + Calendar date = ISO8601.parse(toString()); + if (date == null) { + throw new IllegalArgumentException("Not a date string: " + toString()); + } + return date; + } + + /** + * Convert to date. This default implementation is based on {@code ISO8601.parse(String)}. + * @return date representation of the converted value + * @throws IllegalArgumentException if the string cannot be parsed into a date + */ + public String toDate() { + return convert(toCalendar()).toString(); + } + + /** + * Convert to boolean. This default implementation is based on {@code Boolean.parseBoolean(String)}. + * @return boolean representation of the converted value + */ + public boolean toBoolean() { + return Boolean.parseBoolean(toString()); + } + + /** + * Convert to decimal. This default implementation is based on {@code new BigDecimal(String)}. + * @return decimal representation of the converted value + * @throws NumberFormatException + */ + public BigDecimal toDecimal() { + return new BigDecimal(toString()); + } + } + + /** + * Create a converter for a string. + * @param value The string to convert + * @return A converter for {@code value} + * @throws NumberFormatException + */ + public static Converter convert(final String value) { + return new Converter() { + @Override + public String toString() { + return value; + } + }; + } + + /** + * Create a converter for a binary. + * For the conversion to {@code String} the binary in interpreted as UTF-8 encoded string. + * @param value The binary to convert + * @return A converter for {@code value} + * @throws IllegalArgumentException if the binary is inaccessible + */ + public static Converter convert(final Blob value) { + return new Converter() { + @Override + public String toString() { + try { + InputStream in = value.getNewStream(); + try { + return new String(ByteStreams.toByteArray(in), Charsets.UTF_8); + } + finally { + in.close(); + } + } + catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public Blob toBinary() { + return value; + } + }; + } + + /** + * Create a converter for a long. {@code String.valueOf(long)} is used for the conversion to {@code String}. + * The conversions to {@code double} and {@code long} return the {@code value} itself. + * The conversion to decimal uses {@code new BigDecimal.valueOf(long)}. + * The conversion to date interprets the value as number of milliseconds since {@code 1970-01-01T00:00:00.000Z}. + * @param value The long to convert + * @return A converter for {@code value} + */ + public static Converter convert(final long value) { + return new Converter() { + @Override + public String toString() { + return String.valueOf(value); + } + + @Override + public long toLong() { + return value; + } + + @Override + public double toDouble() { + return value; + } + + @Override + public Calendar toCalendar() { + Calendar date = Calendar.getInstance(UTC); + date.setTimeInMillis(value); + return date; + } + + @Override + public BigDecimal toDecimal() { + return BigDecimal.valueOf(value); + } + }; + } + + /** + * Create a converter for a double. {@code String.valueOf(double)} is used for the conversion to {@code String}. + * The conversions to {@code double} and {@code long} return the {@code value} itself where in the former case + * the value is casted to {@code long}. + * The conversion to decimal uses {@code BigDecimal.valueOf(double)}. + * The conversion to date interprets {@code toLong()} as number of milliseconds since + * {@code 1970-01-01T00:00:00.000Z}. + * @param value The double to convert + * @return A converter for {@code value} + */ + public static Converter convert(final double value) { + return new Converter() { + @Override + public String toString() { + return String.valueOf(value); + } + + @Override + public long toLong() { + return (long) value; + } + + @Override + public double toDouble() { + return value; + } + + @Override + public Calendar toCalendar() { + Calendar date = Calendar.getInstance(TimeZone.getTimeZone("GMT+00:00")); + date.setTimeInMillis(toLong()); + return date; + } + + @Override + public BigDecimal toDecimal() { + return BigDecimal.valueOf(value); + } + }; + } + + /** + * Create a converter for a date. {@code ISO8601.format(Calendar)} is used for the conversion to {@code String}. + * The conversions to {@code double}, {@code long} and {@code BigDecimal} return the number of milliseconds + * since {@code 1970-01-01T00:00:00.000Z}. + * @param value The date to convert + * @return A converter for {@code value} + */ + public static Converter convert(final Calendar value) { + return new Converter() { + @Override + public String toString() { + return ISO8601.format(value); + } + + @Override + public long toLong() { + return value.getTimeInMillis(); + } + + @Override + public double toDouble() { + return value.getTimeInMillis(); + } + + @Override + public Calendar toCalendar() { + return value; + } + + @Override + public BigDecimal toDecimal() { + return new BigDecimal(value.getTimeInMillis()); + } + }; + } + + /** + * Create a converter for a boolean. {@code Boolean.toString(boolean)} is used for the conversion to {@code String}. + * @param value The boolean to convert + * @return A converter for {@code value} + */ + public static Converter convert(final boolean value) { + return new Converter() { + @Override + public String toString() { + return Boolean.toString(value); + } + + @Override + public boolean toBoolean() { + return value; + } + }; + } + + /** + * Create a converter for a decimal. {@code BigDecimal.toString()} is used for the conversion to {@code String}. + * {@code BigDecimal.longValue()} and {@code BigDecimal.doubleValue()} is used for the conversions to + * {@code long} and {@code double}, respectively. + * The conversion to date interprets {@code toLong()} as number of milliseconds since + * {@code 1970-01-01T00:00:00.000Z}. + * @param value The decimal to convert + * @return A converter for {@code value} + */ + public static Converter convert(final BigDecimal value) { + return new Converter() { + @Override + public String toString() { + return value.toString(); + } + + @Override + public long toLong() { + return value.longValue(); + } + + @Override + public double toDouble() { + return value.doubleValue(); + } + + @Override + public Calendar toCalendar() { + Calendar date = Calendar.getInstance(TimeZone.getTimeZone("GMT+00:00")); + date.setTimeInMillis(toLong()); + return date; + } + + @Override + public BigDecimal toDecimal() { + return value; + } + }; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/value/ValueFactoryImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/value/ValueFactoryImpl.java new file mode 100644 index 00000000000..50836786ef0 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/value/ValueFactoryImpl.java @@ -0,0 +1,336 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.value; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Calendar; +import java.util.List; +import javax.jcr.Binary; +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.ValueFormatException; + +import com.google.common.collect.Lists; +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.api.BlobFactory; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager; +import org.apache.jackrabbit.oak.plugins.memory.BinaryPropertyState; +import org.apache.jackrabbit.oak.plugins.memory.BooleanPropertyState; +import org.apache.jackrabbit.oak.plugins.memory.DecimalPropertyState; +import org.apache.jackrabbit.oak.plugins.memory.DoublePropertyState; +import org.apache.jackrabbit.oak.plugins.memory.GenericPropertyState; +import org.apache.jackrabbit.oak.plugins.memory.LongPropertyState; +import org.apache.jackrabbit.oak.plugins.memory.StringPropertyState; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; +import org.apache.jackrabbit.util.ISO8601; + +/** + * Implementation of {@link ValueFactory} interface. + */ +public class ValueFactoryImpl implements ValueFactory { + + private final BlobFactory blobFactory; + private final NamePathMapper namePathMapper; + + /** + * Creates a new instance of {@code ValueFactory}. + * + * @param blobFactory The factory for creation of binary values + * @param namePathMapper The name/path mapping used for converting JCR names/paths to + * the internal representation. + */ + public ValueFactoryImpl(BlobFactory blobFactory, NamePathMapper namePathMapper) { + this.blobFactory = blobFactory; + this.namePathMapper = namePathMapper; + } + + /** + * Utility method for creating a {@code Value} based on a {@code PropertyState}. + * @param property The property state + * @param namePathMapper The name/path mapping used for converting JCR names/paths to + * the internal representation. + * @return New {@code Value} instance + * @throws IllegalArgumentException if {@code property.isArray()} is {@code true}. + */ + public static Value createValue(PropertyState property, NamePathMapper namePathMapper) { + return new ValueImpl(property, namePathMapper); + } + + /** + * Utility method for creating a {@code Value} based on a {@code PropertyValue}. + * @param property The property value + * @param namePathMapper The name/path mapping used for converting JCR names/paths to + * the internal representation. + * @return New {@code Value} instance + * @throws IllegalArgumentException if {@code property.isArray()} is {@code true}. + */ + public static Value createValue(PropertyValue property, NamePathMapper namePathMapper) { + return new ValueImpl(PropertyValues.create(property), namePathMapper); + } + + /** + * Utility method for creating {@code Value}s based on a {@code PropertyState}. + * @param property The property state + * @param namePathMapper The name/path mapping used for converting JCR names/paths to + * the internal representation. + * @return A list of new {@code Value} instances + */ + public static List createValues(PropertyState property, NamePathMapper namePathMapper) { + List values = Lists.newArrayList(); + for (int i = 0; i < property.count(); i++) { + values.add(new ValueImpl(property, i, namePathMapper)); + } + return values; + } + + //-------------------------------------------------------< ValueFactory >--- + + @Override + public Value createValue(String value) { + return new ValueImpl(StringPropertyState.stringProperty("", value), namePathMapper); + } + + @Override + public Value createValue(InputStream value) { + try { + return createBinaryValue(value); + } catch (IOException e) { + return new ErrorValue(e, PropertyType.BINARY); + } + } + + @Override + public Value createValue(Binary value) { + try { + if (value instanceof BinaryImpl) { + // No need to create the value again if we have it already underlying the binary + return ((BinaryImpl) value).getBinaryValue(); + } else { + return createBinaryValue(value.getStream()); + } + } catch (RepositoryException e) { + return new ErrorValue(e, PropertyType.BINARY); + } catch (IOException e) { + return new ErrorValue(e, PropertyType.BINARY); + } + } + + @Override + public Value createValue(long value) { + return new ValueImpl(LongPropertyState.createLongProperty("", value), namePathMapper); + } + + @Override + public Value createValue(double value) { + return new ValueImpl(DoublePropertyState.doubleProperty("", value), namePathMapper); + } + + @Override + public Value createValue(Calendar value) { + return new ValueImpl(LongPropertyState.createDateProperty("", value), namePathMapper); + } + + @Override + public Value createValue(boolean value) { + return new ValueImpl(BooleanPropertyState.booleanProperty("", value), namePathMapper); + } + + @Override + public Value createValue(Node value) throws RepositoryException { + return createValue(value, false); + } + + @Override + public Value createValue(Node value, boolean weak) throws RepositoryException { + return weak + ? new ValueImpl(GenericPropertyState.weakreferenceProperty("", value.getUUID()), namePathMapper) + : new ValueImpl(GenericPropertyState.referenceProperty("", value.getUUID()), namePathMapper); + } + + @Override + public Value createValue(BigDecimal value) { + return new ValueImpl(DecimalPropertyState.decimalProperty("", value), namePathMapper); + } + + @Override + public Value createValue(String value, int type) throws ValueFormatException { + if (value == null) { + throw new ValueFormatException("null"); + } + + try { + switch (type) { + case PropertyType.STRING: + return createValue(value); + case PropertyType.BINARY: + return new ValueImpl(BinaryPropertyState.binaryProperty("", value), namePathMapper); + case PropertyType.LONG: + return createValue(Conversions.convert(value).toLong()); + case PropertyType.DOUBLE: + return createValue(Conversions.convert(value).toDouble()); + case PropertyType.DATE: + if (ISO8601.parse(value) == null) { + throw new ValueFormatException("Invalid date " + value); + } + return new ValueImpl(LongPropertyState.createDateProperty("", value), namePathMapper); + case PropertyType.BOOLEAN: + return createValue(Conversions.convert(value).toBoolean()); + case PropertyType.NAME: + String oakName = namePathMapper.getOakName(value); + if (oakName == null) { + throw new ValueFormatException("Invalid name: " + value); + } + return new ValueImpl(GenericPropertyState.nameProperty("", oakName), namePathMapper); + case PropertyType.PATH: + String oakValue = value; + if (value.startsWith("[") && value.endsWith("]")) { + // identifier path; do no change + } else { + oakValue = namePathMapper.getOakPath(value); + } + if (oakValue == null) { + throw new ValueFormatException("Invalid path: " + value); + } + return new ValueImpl(GenericPropertyState.pathProperty("", oakValue), namePathMapper); + case PropertyType.REFERENCE: + if (!IdentifierManager.isValidUUID(value)) { + throw new ValueFormatException("Invalid reference value " + value); + } + return new ValueImpl(GenericPropertyState.referenceProperty("", value), namePathMapper); + case PropertyType.WEAKREFERENCE: + if (!IdentifierManager.isValidUUID(value)) { + throw new ValueFormatException("Invalid weak reference value " + value); + } + return new ValueImpl(GenericPropertyState.weakreferenceProperty("", value), namePathMapper); + case PropertyType.URI: + new URI(value); + return new ValueImpl(GenericPropertyState.uriProperty("", value), namePathMapper); + case PropertyType.DECIMAL: + return createValue(Conversions.convert(value).toDecimal()); + default: + throw new ValueFormatException("Invalid type: " + type); + } + } catch (NumberFormatException e) { + throw new ValueFormatException("Invalid value " + value + " for type " + PropertyType.nameFromValue(type), e); + } catch (URISyntaxException e) { + throw new ValueFormatException("Invalid value " + value + " for type " + PropertyType.nameFromValue(type), e); + } + } + + @Override + public Binary createBinary(InputStream stream) throws RepositoryException { + try { + return new BinaryImpl(createBinaryValue(stream)); + } catch (IOException e) { + throw new RepositoryException(e); + } + } + + private ValueImpl createBinaryValue(InputStream value) throws IOException { + Blob blob = blobFactory.createBlob(value); + return new ValueImpl(BinaryPropertyState.binaryProperty("", blob), namePathMapper); + } + + //------------------------------------------------------------< ErrorValue >--- + + /** + * Instances of this class represent a {@code Value} which couldn't be retrieved. + * All its accessors throw a {@code RepositoryException}. + */ + private static class ErrorValue implements Value { + private final Exception exception; + private final int type; + + private ErrorValue(Exception exception, int type) { + this.exception = exception; + this.type = type; + } + + @Override + public String getString() throws RepositoryException { + throw createException(); + } + + @Override + public InputStream getStream() throws RepositoryException { + throw createException(); + } + + @Override + public Binary getBinary() throws RepositoryException { + throw createException(); + } + + @Override + public long getLong() throws RepositoryException { + throw createException(); + } + + @Override + public double getDouble() throws RepositoryException { + throw createException(); + } + + @Override + public BigDecimal getDecimal() throws RepositoryException { + throw createException(); + } + + @Override + public Calendar getDate() throws RepositoryException { + throw createException(); + } + + @Override + public boolean getBoolean() throws RepositoryException { + throw createException(); + } + + @Override + public int getType() { + return type; + } + + private RepositoryException createException() { + return new RepositoryException("Inaccessible value", exception); + } + + /** + * Error values are never equal. + * @return {@code false} + */ + @Override + public boolean equals(Object obj) { + return false; + } + + @Override + public String toString() { + return "Inaccessible value: " + exception.getMessage(); + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/value/ValueImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/value/ValueImpl.java new file mode 100644 index 00000000000..59ea8460bb8 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/value/ValueImpl.java @@ -0,0 +1,317 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.value; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.Calendar; + +import javax.jcr.Binary; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +/** + * Implementation of {@link Value} based on {@code PropertyState}. + */ +public class ValueImpl implements Value { + private static final Logger log = LoggerFactory.getLogger(ValueImpl.class); + + private final PropertyState propertyState; + private final int index; + private final NamePathMapper namePathMapper; + + private InputStream stream = null; + + /** + * Create a new {@code Value} instance + * @param property The property state this instance is based on + * @param index The index + * @param namePathMapper The name/path mapping used for converting JCR names/paths to + * the internal representation. + * @throws IllegalArgumentException if {@code index < propertyState.count()} + */ + ValueImpl(PropertyState property, int index, NamePathMapper namePathMapper) { + checkArgument(index < property.count()); + this.propertyState = property; + this.index = index; + this.namePathMapper = namePathMapper; + } + + /** + * Create a new {@code Value} instance + * @param property The property state this instance is based on + * @param namePathMapper The name/path mapping used for converting JCR names/paths to + * the internal representation. + * @throws IllegalArgumentException if {@code property.isArray()} is {@code true}. + */ + ValueImpl(PropertyState property, NamePathMapper namePathMapper) { + this(checkSingleValued(property), 0, namePathMapper); + } + + private static PropertyState checkSingleValued(PropertyState property) { + checkArgument(!property.isArray()); + return property; + } + + //--------------------------------------------------------------< Value >--- + + /** + * @see javax.jcr.Value#getType() + */ + @Override + public int getType() { + return propertyState.getType().tag(); + } + + /** + * @see javax.jcr.Value#getBoolean() + */ + @Override + public boolean getBoolean() throws RepositoryException { + switch (getType()) { + case PropertyType.STRING: + case PropertyType.BINARY: + case PropertyType.BOOLEAN: + return propertyState.getValue(Type.BOOLEAN, index); + default: + throw new ValueFormatException("Incompatible type " + PropertyType.nameFromValue(getType())); + } + } + + /** + * @see javax.jcr.Value#getDate() + */ + @Override + public Calendar getDate() throws RepositoryException { + try { + switch (getType()) { + case PropertyType.STRING: + case PropertyType.BINARY: + String value = propertyState.getValue(Type.DATE, index); + return Conversions.convert(value).toCalendar(); + case PropertyType.LONG: + case PropertyType.DOUBLE: + case PropertyType.DATE: + case PropertyType.DECIMAL: + return Conversions.convert(propertyState.getValue(Type.LONG, index)).toCalendar(); + default: + throw new ValueFormatException("Incompatible type " + PropertyType.nameFromValue(getType())); + } + } + catch (IllegalArgumentException e) { + throw new ValueFormatException("Error converting value to date", e); + } + } + + /** + * @see javax.jcr.Value#getDecimal() + */ + @Override + public BigDecimal getDecimal() throws RepositoryException { + try { + switch (getType()) { + case PropertyType.STRING: + case PropertyType.BINARY: + case PropertyType.LONG: + case PropertyType.DOUBLE: + case PropertyType.DATE: + case PropertyType.DECIMAL: + return propertyState.getValue(Type.DECIMAL, index); + default: + throw new ValueFormatException("Incompatible type " + PropertyType.nameFromValue(getType())); + } + } + catch (IllegalArgumentException e) { + throw new ValueFormatException("Error converting value to decimal", e); + } + } + + /** + * @see javax.jcr.Value#getDouble() + */ + @Override + public double getDouble() throws RepositoryException { + try { + switch (getType()) { + case PropertyType.STRING: + case PropertyType.BINARY: + case PropertyType.LONG: + case PropertyType.DOUBLE: + case PropertyType.DATE: + case PropertyType.DECIMAL: + return propertyState.getValue(Type.DOUBLE, index); + default: + throw new ValueFormatException("Incompatible type " + PropertyType.nameFromValue(getType())); + } + } + catch (IllegalArgumentException e) { + throw new ValueFormatException("Error converting value to double", e); + } + } + + /** + * @see javax.jcr.Value#getLong() + */ + @Override + public long getLong() throws RepositoryException { + try { + switch (getType()) { + case PropertyType.STRING: + case PropertyType.BINARY: + case PropertyType.LONG: + case PropertyType.DOUBLE: + case PropertyType.DATE: + case PropertyType.DECIMAL: + return propertyState.getValue(Type.LONG, index); + default: + throw new ValueFormatException("Incompatible type " + PropertyType.nameFromValue(getType())); + } + } + catch (IllegalArgumentException e) { + throw new ValueFormatException("Error converting value to double", e); + } + } + + /** + * @see javax.jcr.Value#getString() + */ + @Override + public String getString() throws RepositoryException { + checkState(getType() != PropertyType.BINARY || stream == null, + "getStream has previously been called on this Value instance. " + + "In this case a new Value instance must be acquired in order to successfully call this method."); + + switch (getType()) { + case PropertyType.NAME: + return namePathMapper.getJcrName(propertyState.getValue(Type.STRING, index)); + case PropertyType.PATH: + String s = propertyState.getValue(Type.STRING, index); + if (s.startsWith("[") && s.endsWith("]")) { + // identifier paths are returned as-is (JCR 2.0, 3.4.3.1) + return s; + } else { + return namePathMapper.getJcrPath(s); + } + default: + return propertyState.getValue(Type.STRING, index); + } + } + + /** + * @see javax.jcr.Value#getStream() + */ + @Override + public InputStream getStream() throws IllegalStateException, RepositoryException { + if (stream == null) { + stream = getNewStream(); + } + return stream; + } + + InputStream getNewStream() throws RepositoryException { + return propertyState.getValue(Type.BINARY, index).getNewStream(); + } + + long getStreamLength() { + return propertyState.getValue(Type.BINARY, index).length(); + } + + /** + * @see javax.jcr.Value#getBinary() + */ + @Override + public Binary getBinary() throws RepositoryException { + return new BinaryImpl(this); + } + + //-------------------------------------------------------------< Object >--- + + /** + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object other) { + if (other instanceof ValueImpl) { + ValueImpl that = (ValueImpl) other; + return compare(propertyState, index, that.propertyState, that.index) == 0; + } else { + return false; + } + } + + /** + * @see Object#hashCode() + */ + @Override + public int hashCode() { + if (getType() == PropertyType.BINARY) { + return propertyState.getValue(Type.BINARY, index).hashCode(); + } + else { + return propertyState.getValue(Type.STRING, index).hashCode(); + } + } + + @Override + public String toString() { + return propertyState.getValue(Type.STRING, index); + } + + private static int compare(PropertyState p1, int i1, PropertyState p2, int i2) { + if (p1.getType().tag() != p2.getType().tag()) { + return Integer.signum(p1.getType().tag() - p2.getType().tag()); + } + switch (p1.getType().tag()) { + case PropertyType.BINARY: + return compare(p1.getValue(Type.BINARY, i1), p2.getValue(Type.BINARY, i2)); + case PropertyType.DOUBLE: + return compare(p1.getValue(Type.DOUBLE, i1), p2.getValue(Type.DOUBLE, i2)); + case PropertyType.LONG: + return compare(p1.getValue(Type.LONG, i1), p2.getValue(Type.LONG, i2)); + case PropertyType.DECIMAL: + return compare(p1.getValue(Type.DECIMAL, i1), p2.getValue(Type.DECIMAL, i2)); + case PropertyType.DATE: + return compareAsDate(p1.getValue(Type.STRING, i1), p2.getValue(Type.STRING, i2)); + default: + return compare(p1.getValue(Type.STRING, i1), p2.getValue(Type.STRING, i2)); + } + } + + private static > int compare(T p1, T p2) { + return p1.compareTo(p2); + } + + private static int compareAsDate(String p1, String p2) { + Calendar c1 = Conversions.convert(p1).toCalendar(); + Calendar c2 = Conversions.convert(p1).toCalendar(); + return c1 != null && c2 != null + ? c1.compareTo(c2) + : p1.compareTo(p2); + } + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/Query.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/Query.java new file mode 100644 index 00000000000..464e81a95e6 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/Query.java @@ -0,0 +1,613 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law + * or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.apache.jackrabbit.oak.query; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.query.ast.AstVisitorBase; +import org.apache.jackrabbit.oak.query.ast.BindVariableValueImpl; +import org.apache.jackrabbit.oak.query.ast.ChildNodeImpl; +import org.apache.jackrabbit.oak.query.ast.ChildNodeJoinConditionImpl; +import org.apache.jackrabbit.oak.query.ast.ColumnImpl; +import org.apache.jackrabbit.oak.query.ast.ComparisonImpl; +import org.apache.jackrabbit.oak.query.ast.ConstraintImpl; +import org.apache.jackrabbit.oak.query.ast.DescendantNodeImpl; +import org.apache.jackrabbit.oak.query.ast.DescendantNodeJoinConditionImpl; +import org.apache.jackrabbit.oak.query.ast.EquiJoinConditionImpl; +import org.apache.jackrabbit.oak.query.ast.FullTextSearchImpl; +import org.apache.jackrabbit.oak.query.ast.FullTextSearchScoreImpl; +import org.apache.jackrabbit.oak.query.ast.LengthImpl; +import org.apache.jackrabbit.oak.query.ast.LiteralImpl; +import org.apache.jackrabbit.oak.query.ast.LowerCaseImpl; +import org.apache.jackrabbit.oak.query.ast.NodeLocalNameImpl; +import org.apache.jackrabbit.oak.query.ast.NodeNameImpl; +import org.apache.jackrabbit.oak.query.ast.OrderingImpl; +import org.apache.jackrabbit.oak.query.ast.PropertyExistenceImpl; +import org.apache.jackrabbit.oak.query.ast.PropertyValueImpl; +import org.apache.jackrabbit.oak.query.ast.SameNodeImpl; +import org.apache.jackrabbit.oak.query.ast.SameNodeJoinConditionImpl; +import org.apache.jackrabbit.oak.query.ast.SelectorImpl; +import org.apache.jackrabbit.oak.query.ast.SourceImpl; +import org.apache.jackrabbit.oak.query.ast.UpperCaseImpl; +import org.apache.jackrabbit.oak.spi.query.Filter; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; +import org.apache.jackrabbit.oak.spi.query.QueryIndex; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * Represents a parsed query. Lifecycle: use the constructor to create a new + * object. Call init() to initialize the bind variable map. If the query is + * re-executed, a new instance is created. + */ +public class Query { + + /** + * The "jcr:path" pseudo-property. + */ + // TODO jcr:path isn't an official feature, support it? + public static final String JCR_PATH = "jcr:path"; + + /** + * The "jcr:score" pseudo-property. + */ + public static final String JCR_SCORE = "jcr:score"; + + final SourceImpl source; + final ConstraintImpl constraint; + final HashMap bindVariableMap = new HashMap(); + final HashMap selectorIndexes = new HashMap(); + final ArrayList selectors = new ArrayList(); + + private QueryEngineImpl queryEngine; + private final OrderingImpl[] orderings; + private ColumnImpl[] columns; + private boolean explain, measure; + private long limit = Long.MAX_VALUE; + private long offset; + private long size = -1; + private boolean prepared; + private Root root; + private NamePathMapper namePathMapper; + + Query(SourceImpl source, ConstraintImpl constraint, OrderingImpl[] orderings, + ColumnImpl[] columns) { + this.source = source; + this.constraint = constraint; + this.orderings = orderings; + this.columns = columns; + } + + public void init() { + + final Query query = this; + + new AstVisitorBase() { + + @Override + public boolean visit(BindVariableValueImpl node) { + node.setQuery(query); + bindVariableMap.put(node.getBindVariableName(), null); + return true; + } + + @Override + public boolean visit(ChildNodeImpl node) { + node.setQuery(query); + node.bindSelector(source); + return true; + } + + @Override + public boolean visit(ChildNodeJoinConditionImpl node) { + node.setQuery(query); + node.bindSelector(source); + return true; + } + + @Override + public boolean visit(ColumnImpl node) { + node.setQuery(query); + return true; + } + + @Override + public boolean visit(DescendantNodeImpl node) { + node.setQuery(query); + node.bindSelector(source); + return true; + } + + @Override + public boolean visit(DescendantNodeJoinConditionImpl node) { + node.setQuery(query); + node.bindSelector(source); + return true; + } + + @Override + public boolean visit(EquiJoinConditionImpl node) { + node.setQuery(query); + node.bindSelector(source); + return true; + } + + @Override + public boolean visit(FullTextSearchImpl node) { + node.setQuery(query); + node.bindSelector(source); + return super.visit(node); + } + + @Override + public boolean visit(FullTextSearchScoreImpl node) { + node.setQuery(query); + node.bindSelector(source); + return true; + } + + @Override + public boolean visit(LiteralImpl node) { + node.setQuery(query); + return true; + } + + @Override + public boolean visit(NodeLocalNameImpl node) { + node.setQuery(query); + node.bindSelector(source); + return true; + } + + @Override + public boolean visit(NodeNameImpl node) { + node.setQuery(query); + node.bindSelector(source); + return true; + } + + @Override + public boolean visit(PropertyExistenceImpl node) { + node.setQuery(query); + node.bindSelector(source); + return true; + } + + @Override + public boolean visit(PropertyValueImpl node) { + node.setQuery(query); + node.bindSelector(source); + return true; + } + + @Override + public boolean visit(SameNodeImpl node) { + node.setQuery(query); + node.bindSelector(source); + return true; + } + + @Override + public boolean visit(SameNodeJoinConditionImpl node) { + node.setQuery(query); + node.bindSelector(source); + return true; + } + + @Override + public boolean visit(SelectorImpl node) { + String name = node.getSelectorName(); + if (selectorIndexes.put(name, selectors.size()) != null) { + throw new IllegalArgumentException("Two selectors with the same name: " + name); + } + selectors.add(node); + node.setQuery(query); + return true; + } + + @Override + public boolean visit(LengthImpl node) { + node.setQuery(query); + return super.visit(node); + } + + @Override + public boolean visit(UpperCaseImpl node) { + node.setQuery(query); + return super.visit(node); + } + + @Override + public boolean visit(LowerCaseImpl node) { + node.setQuery(query); + return super.visit(node); + } + + @Override + public boolean visit(ComparisonImpl node) { + node.setQuery(query); + return super.visit(node); + } + + }.visit(this); + source.setQueryConstraint(constraint); + source.init(this); + for (ColumnImpl column : columns) { + column.bindSelector(source); + } + } + + public ColumnImpl[] getColumns() { + return columns; + } + + public ConstraintImpl getConstraint() { + return constraint; + } + + public OrderingImpl[] getOrderings() { + return orderings; + } + + public SourceImpl getSource() { + return source; + } + + void bindValue(String varName, PropertyValue value) { + bindVariableMap.put(varName, value); + } + + public void setLimit(long limit) { + this.limit = limit; + } + + public void setOffset(long offset) { + this.offset = offset; + } + + public void setExplain(boolean explain) { + this.explain = explain; + } + + public void setMeasure(boolean measure) { + this.measure = measure; + } + + public ResultImpl executeQuery(NodeState root) { + return new ResultImpl(this, root); + } + + Iterator getRows(NodeState root) { + prepare(); + Iterator it; + if (explain) { + String plan = source.getPlan(root); + columns = new ColumnImpl[] { new ColumnImpl("explain", "plan", "plan")}; + ResultRowImpl r = new ResultRowImpl(this, + new String[0], + new PropertyValue[] { PropertyValues.newString(plan)}, + null); + it = Arrays.asList(r).iterator(); + } else { + if (orderings == null) { + // can apply limit and offset directly + it = new RowIterator(root, limit, offset); + } else { + // read and order first; skip and limit afterwards + it = new RowIterator(root, Long.MAX_VALUE, 0); + } + long resultCount = 0; + if (orderings != null) { + // TODO "order by" is not necessary if the used index returns + // rows in the same order + ArrayList list = new ArrayList(); + while (it.hasNext()) { + ResultRowImpl r = it.next(); + list.add(r); + } + Collections.sort(list); + // if limit is set, possibly remove the tailing entries + // for list size 10, offset 2, limit 5: remove 3 + resultCount = list.size(); + // avoid overflow (both offset and limit could be Long.MAX_VALUE) + long keep = Math.min(list.size(), offset) + Math.min(list.size(), limit); + while (list.size() > keep) { + // remove tail entries right now, to save memory (don't copy) + // remove the entries starting at the end, + // to avoid n^2 performance + list.remove(list.size() - 1); + } + it = list.iterator(); + // skip the head (this is more efficient than removing + // if there are many entries) + for (int i = 0; i < offset && it.hasNext(); i++) { + it.next(); + } + size = list.size() - offset; + } else if (measure) { + while (it.hasNext()) { + resultCount++; + it.next(); + } + } + if (measure) { + columns = new ColumnImpl[] { + new ColumnImpl("measure", "selector", "selector"), + new ColumnImpl("measure", "scanCount", "scanCount") + }; + ArrayList list = new ArrayList(); + ResultRowImpl r = new ResultRowImpl(this, + new String[0], + new PropertyValue[] { + PropertyValues.newString("query"), + PropertyValues.newLong(resultCount) + }, + null); + list.add(r); + for (SelectorImpl selector : selectors) { + r = new ResultRowImpl(this, + new String[0], + new PropertyValue[] { + PropertyValues.newString(selector.getSelectorName()), + PropertyValues.newLong(selector.getScanCount()), + }, + null); + list.add(r); + } + it = list.iterator(); + } + } + return it; + } + + public int compareRows(PropertyValue[] orderValues, + PropertyValue[] orderValues2) { + int comp = 0; + for (int i = 0, size = orderings.length; i < size; i++) { + PropertyValue a = orderValues[i]; + PropertyValue b = orderValues2[i]; + if (a == null || b == null) { + if (a == b) { + comp = 0; + } else if (a == null) { + // TODO order by: nulls first (it looks like), or low? + comp = -1; + } else { + comp = 1; + } + } else { + comp = a.compareTo(b); + } + if (comp != 0) { + if (orderings[i].isDescending()) { + comp = -comp; + } + break; + } + } + return comp; + } + + void prepare() { + if (prepared) { + return; + } + prepared = true; + source.prepare(); + } + + /** + * An iterator over result rows. + */ + class RowIterator implements Iterator { + + private final NodeState root; + private ResultRowImpl current; + private boolean started, end; + private long limit, offset, rowIndex; + + RowIterator(NodeState root, long limit, long offset) { + this.root = root; + this.limit = limit; + this.offset = offset; + } + + private void fetchNext() { + if (end) { + return; + } + if (rowIndex >= limit) { + end = true; + return; + } + if (!started) { + source.execute(root); + started = true; + } + while (true) { + if (source.next()) { + if (constraint == null || constraint.evaluate()) { + if (offset > 0) { + offset--; + continue; + } + current = currentRow(); + rowIndex++; + break; + } + } else { + current = null; + end = true; + break; + } + } + } + + @Override + public boolean hasNext() { + if (end) { + return false; + } + if (current == null) { + fetchNext(); + } + return !end; + } + + @Override + public ResultRowImpl next() { + if (end) { + return null; + } + if (current == null) { + fetchNext(); + } + ResultRowImpl r = current; + current = null; + return r; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + } + + ResultRowImpl currentRow() { + int selectorCount = selectors.size(); + String[] paths = new String[selectorCount]; + for (int i = 0; i < selectorCount; i++) { + SelectorImpl s = selectors.get(i); + paths[i] = s.currentPath(); + } + int columnCount = columns.length; + PropertyValue[] values = new PropertyValue[columnCount]; + for (int i = 0; i < columnCount; i++) { + ColumnImpl c = columns[i]; + values[i] = c.currentProperty(); + } + PropertyValue[] orderValues; + if (orderings == null) { + orderValues = null; + } else { + int size = orderings.length; + orderValues = new PropertyValue[size]; + for (int i = 0; i < size; i++) { + orderValues[i] = orderings[i].getOperand().currentProperty(); + } + } + return new ResultRowImpl(this, paths, values, orderValues); + } + + public int getSelectorIndex(String selectorName) { + Integer index = selectorIndexes.get(selectorName); + if (index == null) { + throw new IllegalArgumentException("Unknown selector: " + selectorName); + } + return index; + } + + public int getColumnIndex(String columnName) { + for (int i = 0, size = columns.length; i < size; i++) { + ColumnImpl c = columns[i]; + String cn = c.getColumnName(); + if (cn != null && cn.equals(columnName)) { + return i; + } + } + throw new IllegalArgumentException("Column not found: " + columnName); + } + + public PropertyValue getBindVariableValue(String bindVariableName) { + PropertyValue v = bindVariableMap.get(bindVariableName); + if (v == null) { + throw new IllegalArgumentException("Bind variable value not set: " + bindVariableName); + } + return v; + } + + public List getSelectors() { + return Collections.unmodifiableList(selectors); + } + + public List getBindVariableNames() { + return new ArrayList(bindVariableMap.keySet()); + } + + public void setQueryEngine(QueryEngineImpl queryEngine) { + this.queryEngine = queryEngine; + } + + public QueryIndex getBestIndex(Filter filter) { + return queryEngine.getBestIndex(filter); + } + + public void setRoot(Root root) { + this.root = root; + } + + public void setNamePathMapper(NamePathMapper namePathMapper) { + this.namePathMapper = namePathMapper; + } + + public NamePathMapper getNamePathMapper() { + return namePathMapper; + } + + public Tree getTree(String path) { + return root.getTree(path); + } + + @Override + public String toString() { + StringBuilder buff = new StringBuilder(); + buff.append("select "); + int i = 0; + for (ColumnImpl c : columns) { + if (i++ > 0) { + buff.append(", "); + } + buff.append(c); + } + buff.append(" from ").append(source); + if (constraint != null) { + buff.append(" where ").append(constraint); + } + if (orderings != null) { + buff.append(" order by "); + i = 0; + for (OrderingImpl o : orderings) { + if (i++ > 0) { + buff.append(", "); + } + buff.append(o); + } + } + return buff.toString(); + } + + public long getSize() { + return size; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineImpl.java new file mode 100644 index 00000000000..e1f8933b931 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineImpl.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.query; + +import java.text.ParseException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.query.index.TraversingIndex; +import org.apache.jackrabbit.oak.spi.query.Filter; +import org.apache.jackrabbit.oak.spi.query.QueryIndex; +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * The query engine implementation. + */ +public class QueryEngineImpl { + + static final String SQL2 = "JCR-SQL2"; + static final String SQL = "sql"; + static final String XPATH = "xpath"; + static final String JQOM = "JCR-JQOM"; + + private final NodeState root; + private final QueryIndexProvider indexProvider; + + public QueryEngineImpl(NodeState root, QueryIndexProvider indexProvider) { + this.root = root; + this.indexProvider = indexProvider; + } + + public List getSupportedQueryLanguages() { + return Arrays.asList(SQL2, SQL, XPATH, JQOM); + } + + /** + * Parse the query (check if it's valid) and get the list of bind variable names. + * + * @param statement + * @param language + * @return the list of bind variable names + * @throws ParseException + */ + public List getBindVariableNames(String statement, String language) throws ParseException { + Query q = parseQuery(statement, language); + return q.getBindVariableNames(); + } + + private Query parseQuery(String statement, String language) throws ParseException { + Query q; + if (SQL2.equals(language) || JQOM.equals(language)) { + SQL2Parser parser = new SQL2Parser(); + q = parser.parse(statement); + } else if (SQL.equals(language)) { + SQL2Parser parser = new SQL2Parser(); + parser.setSupportSQL1(true); + q = parser.parse(statement); + } else if (XPATH.equals(language)) { + XPathToSQL2Converter converter = new XPathToSQL2Converter(); + String sql2 = converter.convert(statement); + SQL2Parser parser = new SQL2Parser(); + try { + q = parser.parse(sql2); + } catch (ParseException e) { + throw new ParseException(statement + " converted to SQL-2 " + e.getMessage(), 0); + } + } else { + throw new ParseException("Unsupported language: " + language, 0); + } + return q; + } + + public ResultImpl executeQuery(String statement, String language, + long limit, long offset, Map bindings, + Root root, + NamePathMapper namePathMapper) throws ParseException { + Query q = parseQuery(statement, language); + q.setRoot(root); + q.setNamePathMapper(namePathMapper); + q.setLimit(limit); + q.setOffset(offset); + if (bindings != null) { + for (Entry e : bindings.entrySet()) { + q.bindValue(e.getKey(), e.getValue()); + } + } + q.setQueryEngine(this); + q.prepare(); + return q.executeQuery(this.root); + } + + public QueryIndex getBestIndex(Filter filter) { + QueryIndex best = null; + double bestCost = Double.MAX_VALUE; + for (QueryIndex index : getIndexes()) { + double cost = index.getCost(filter, root); + if (cost < bestCost) { + bestCost = cost; + best = index; + } + } + if (best == null) { + best = new TraversingIndex(); + } + return best; + } + + private List getIndexes() { + return indexProvider.getQueryIndexes(root); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ResultImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ResultImpl.java new file mode 100644 index 00000000000..6fee7bb14fa --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ResultImpl.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.query; + +import java.util.Iterator; +import java.util.List; +import org.apache.jackrabbit.oak.api.Result; +import org.apache.jackrabbit.oak.api.ResultRow; +import org.apache.jackrabbit.oak.query.ast.ColumnImpl; +import org.apache.jackrabbit.oak.query.ast.SelectorImpl; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * A query result. + */ +public class ResultImpl implements Result { + + protected final Query query; + protected final NodeState root; + + ResultImpl(Query query, NodeState root) { + this.query = query; + this.root = root; + } + + @Override + public String[] getColumnNames() { + ColumnImpl[] cols = query.getColumns(); + String[] names = new String[cols.length]; + for (int i = 0; i < cols.length; i++) { + names[i] = cols[i].getColumnName(); + } + return names; + } + + @Override + public String[] getSelectorNames() { + List selectors = query.getSelectors(); + String[] names = new String[selectors.size()]; + for (int i = 0; i < selectors.size(); i++) { + names[i] = selectors.get(i).getSelectorName(); + } + return names; + } + + @Override + public Iterable getRows() { + return new Iterable() { + + @Override + public Iterator iterator() { + return query.getRows(root); + } + + }; + } + + @Override + public long getSize() { + return query.getSize(); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ResultRowImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ResultRowImpl.java new file mode 100644 index 00000000000..388e1cefa97 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ResultRowImpl.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.query; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.ResultRow; +import org.apache.jackrabbit.oak.query.ast.ColumnImpl; +import org.apache.jackrabbit.oak.query.ast.SelectorImpl; + +/** + * A query result row that keeps all data in memory. + */ +public class ResultRowImpl implements ResultRow, Comparable { + + private final Query query; + private final String[] paths; + private final PropertyValue[] values; + private final PropertyValue[] orderValues; + + ResultRowImpl(Query query, String[] paths, PropertyValue[] values, PropertyValue[] orderValues) { + this.query = query; + this.paths = paths; + this.values = values; + this.orderValues = orderValues; + } + + @Override + public String getPath() { + if (paths.length > 1) { + throw new IllegalArgumentException("More than one selector"); + } else if (paths.length == 0) { + throw new IllegalArgumentException("This query does not have a selector"); + } + return paths[0]; + } + + @Override + public String getPath(String selectorName) { + int index = query.getSelectorIndex(selectorName); + if (paths == null || index >= paths.length) { + return null; + } + return paths[index]; + } + + @Override + public PropertyValue getValue(String columnName) { + return values[query.getColumnIndex(columnName)]; + } + + @Override + public PropertyValue[] getValues() { + PropertyValue[] v2 = new PropertyValue[values.length]; + System.arraycopy(values, 0, v2, 0, values.length); + return v2; + } + + @Override + public int compareTo(ResultRowImpl o) { + return query.compareRows(orderValues, o.orderValues); + } + + @Override + public String toString() { + StringBuilder buff = new StringBuilder(); + for (SelectorImpl s : query.getSelectors()) { + String n = s.getSelectorName(); + String p = getPath(n); + if (p != null) { + buff.append(n).append(": ").append(p).append(" "); + } + } + ColumnImpl[] cols = query.getColumns(); + for (int i = 0; i < values.length; i++) { + ColumnImpl c = cols[i]; + String n = c.getColumnName(); + if (n != null) { + buff.append(n).append(": ").append(values[i]).append(" "); + } + } + return buff.toString(); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/SQL2Parser.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/SQL2Parser.java new file mode 100644 index 00000000000..ef7737e54e4 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/SQL2Parser.java @@ -0,0 +1,1160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.query; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.query.ast.AstElementFactory; +import org.apache.jackrabbit.oak.query.ast.BindVariableValueImpl; +import org.apache.jackrabbit.oak.query.ast.ColumnImpl; +import org.apache.jackrabbit.oak.query.ast.ConstraintImpl; +import org.apache.jackrabbit.oak.query.ast.DynamicOperandImpl; +import org.apache.jackrabbit.oak.query.ast.JoinConditionImpl; +import org.apache.jackrabbit.oak.query.ast.JoinType; +import org.apache.jackrabbit.oak.query.ast.LiteralImpl; +import org.apache.jackrabbit.oak.query.ast.Operator; +import org.apache.jackrabbit.oak.query.ast.OrderingImpl; +import org.apache.jackrabbit.oak.query.ast.PropertyExistenceImpl; +import org.apache.jackrabbit.oak.query.ast.PropertyValueImpl; +import org.apache.jackrabbit.oak.query.ast.SelectorImpl; +import org.apache.jackrabbit.oak.query.ast.SourceImpl; +import org.apache.jackrabbit.oak.query.ast.StaticOperandImpl; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; + +import javax.jcr.PropertyType; +import java.math.BigDecimal; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * The SQL2 parser can convert a JCR-SQL2 query to a query. + * The 'old' SQL query language is also supported if + */ +public class SQL2Parser { + + // Character types, used during the tokenizer phase + private static final int CHAR_END = -1, CHAR_VALUE = 2, CHAR_QUOTED = 3; + private static final int CHAR_NAME = 4, CHAR_SPECIAL_1 = 5, CHAR_SPECIAL_2 = 6; + private static final int CHAR_STRING = 7, CHAR_DECIMAL = 8; + + // Token types + private static final int KEYWORD = 1, IDENTIFIER = 2, PARAMETER = 3, END = 4, VALUE = 5; + private static final int MINUS = 12, PLUS = 13, OPEN = 14, CLOSE = 15; + + // The query as an array of characters and character types + private String statement; + private char[] statementChars; + private int[] characterTypes; + + // The current state of the parser + private int parseIndex; + private int currentTokenType; + private String currentToken; + private boolean currentTokenQuoted; + private PropertyValue currentValue; + private ArrayList expected; + + // The bind variables + private HashMap bindVariables; + + // The list of selectors of this query + private ArrayList selectors; + + // SQL injection protection: if disabled, literals are not allowed + private boolean allowTextLiterals = true; + private boolean allowNumberLiterals = true; + + private final AstElementFactory factory = new AstElementFactory(); + + private boolean supportSQL1; + + /** + * Create a new parser. A parser can be re-used, but it is not thread safe. + * + */ + public SQL2Parser() { + } + + /** + * Parse the statement and return the query. + * + * @param query the query string + * @return the query + * @throws ParseException if parsing fails + */ + public Query parse(String query) throws ParseException { + // TODO possibly support union,... as available at + // http://docs.jboss.org/modeshape/latest/manuals/reference/html/jcr-query-and-search.html + + initialize(query); + selectors = new ArrayList(); + expected = new ArrayList(); + bindVariables = new HashMap(); + read(); + boolean explain = false, measure = false; + if (readIf("EXPLAIN")) { + explain = true; + } else if (readIf("MEASURE")) { + measure = true; + } + read("SELECT"); + ArrayList list = parseColumns(); + if (supportSQL1) { + addColumnIfNecessary(list, Query.JCR_PATH, Query.JCR_PATH); + addColumnIfNecessary(list, Query.JCR_SCORE, Query.JCR_SCORE); + } + read("FROM"); + SourceImpl source = parseSource(); + ColumnImpl[] columnArray = resolveColumns(list); + ConstraintImpl constraint = null; + if (readIf("WHERE")) { + constraint = parseConstraint(); + } + OrderingImpl[] orderings = null; + if (readIf("ORDER")) { + read("BY"); + orderings = parseOrder(); + } + if (!currentToken.isEmpty()) { + throw getSyntaxError(""); + } + Query q = new Query(source, constraint, orderings, columnArray); + q.setExplain(explain); + q.setMeasure(measure); + try { + q.init(); + } catch (Exception e) { + ParseException e2 = new ParseException(query + ": " + e.getMessage(), 0); + e2.initCause(e); + throw e2; + } + return q; + } + + private static void addColumnIfNecessary(ArrayList list, + String columnName, String propertyName) { + for (ColumnOrWildcard c : list) { + String col = c.columnName; + if (columnName.equals(col)) { + // it already exists + return; + } + } + ColumnOrWildcard column = new ColumnOrWildcard(); + column.columnName = columnName; + column.propertyName = propertyName; + list.add(column); + } + + /** + * Enable or disable support for SQL-1 queries. + * + * @param sql1 the new value + */ + public void setSupportSQL1(boolean sql1) { + this.supportSQL1 = sql1; + } + + private SelectorImpl parseSelector() throws ParseException { + String nodeTypeName = readName(); + if (readIf("AS")) { + String selectorName = readName(); + return factory.selector(nodeTypeName, selectorName); + } else { + return factory.selector(nodeTypeName, nodeTypeName); + } + } + + private String readName() throws ParseException { + if (readIf("[")) { + if (currentTokenType == VALUE) { + PropertyValue value = readString(); + read("]"); + return value.getValue(Type.STRING); + } else { + int level = 1; + StringBuilder buff = new StringBuilder(); + while (true) { + if (isToken("]")) { + if (--level <= 0) { + read(); + break; + } + } else if (isToken("[")) { + level++; + } + buff.append(readAny()); + } + return buff.toString(); + } + } else { + return readAny(); + } + } + + private SourceImpl parseSource() throws ParseException { + SelectorImpl selector = parseSelector(); + selectors.add(selector); + SourceImpl source = selector; + while (true) { + JoinType joinType; + if (readIf("RIGHT")) { + read("OUTER"); + joinType = JoinType.RIGHT_OUTER; + } else if (readIf("LEFT")) { + read("OUTER"); + joinType = JoinType.LEFT_OUTER; + } else if (readIf("INNER")) { + joinType = JoinType.INNER; + } else { + break; + } + read("JOIN"); + selector = parseSelector(); + selectors.add(selector); + read("ON"); + JoinConditionImpl on = parseJoinCondition(); + source = factory.join(source, selector, joinType, on); + } + return source; + } + + private JoinConditionImpl parseJoinCondition() throws ParseException { + boolean identifier = currentTokenType == IDENTIFIER; + String name = readName(); + JoinConditionImpl c; + if (identifier && readIf("(")) { + if ("ISSAMENODE".equalsIgnoreCase(name)) { + String selector1 = readName(); + read(","); + String selector2 = readName(); + if (readIf(",")) { + c = factory.sameNodeJoinCondition(selector1, selector2, readAbsolutePath()); + } else { + // TODO verify "." is correct + c = factory.sameNodeJoinCondition(selector1, selector2, "."); + } + } else if ("ISCHILDNODE".equalsIgnoreCase(name)) { + String childSelector = readName(); + read(","); + c = factory.childNodeJoinCondition(childSelector, readName()); + } else if ("ISDESCENDANTNODE".equalsIgnoreCase(name)) { + String descendantSelector = readName(); + read(","); + c = factory.descendantNodeJoinCondition(descendantSelector, readName()); + } else { + throw getSyntaxError("ISSAMENODE, ISCHILDNODE, or ISDESCENDANTNODE"); + } + read(")"); + return c; + } else { + String selector1 = name; + read("."); + String property1 = readName(); + read("="); + String selector2 = readName(); + read("."); + return factory.equiJoinCondition(selector1, property1, selector2, readName()); + } + } + + private ConstraintImpl parseConstraint() throws ParseException { + ConstraintImpl a = parseAnd(); + while (readIf("OR")) { + a = factory.or(a, parseAnd()); + } + return a; + } + + private ConstraintImpl parseAnd() throws ParseException { + ConstraintImpl a = parseCondition(); + while (readIf("AND")) { + a = factory.and(a, parseCondition()); + } + return a; + } + + private ConstraintImpl parseCondition() throws ParseException { + ConstraintImpl a; + if (readIf("NOT")) { + a = factory.not(parseCondition()); + } else if (readIf("(")) { + a = parseConstraint(); + read(")"); + } else if (currentTokenType == IDENTIFIER) { + String identifier = readName(); + if (readIf("(")) { + a = parseConditionFunctionIf(identifier); + if (a == null) { + DynamicOperandImpl op = parseExpressionFunction(identifier); + a = parseCondition(op); + } + } else if (readIf(".")) { + a = parseCondition(factory.propertyValue(identifier, readName())); + } else { + a = parseCondition(factory.propertyValue(getOnlySelectorName(), identifier)); + } + } else if ("[".equals(currentToken)) { + String name = readName(); + if (readIf(".")) { + a = parseCondition(factory.propertyValue(name, readName())); + } else { + a = parseCondition(factory.propertyValue(getOnlySelectorName(), name)); + } + } else if (supportSQL1) { + StaticOperandImpl left = parseStaticOperand(); + if (readIf("IN")) { + DynamicOperandImpl right = parseDynamicOperand(); + ConstraintImpl c = factory.comparison(right, Operator.EQUAL, left); + return c; + } else { + throw getSyntaxError(); + } + } else { + throw getSyntaxError(); + } + return a; + } + + private ConstraintImpl parseCondition(DynamicOperandImpl left) throws ParseException { + ConstraintImpl c; + if (readIf("=")) { + c = factory.comparison(left, Operator.EQUAL, parseStaticOperand()); + } else if (readIf("<>")) { + c = factory.comparison(left, Operator.NOT_EQUAL, parseStaticOperand()); + } else if (readIf("<")) { + c = factory.comparison(left, Operator.LESS_THAN, parseStaticOperand()); + } else if (readIf(">")) { + c = factory.comparison(left, Operator.GREATER_THAN, parseStaticOperand()); + } else if (readIf("<=")) { + c = factory.comparison(left, Operator.LESS_OR_EQUAL, parseStaticOperand()); + } else if (readIf(">=")) { + c = factory.comparison(left, Operator.GREATER_OR_EQUAL, parseStaticOperand()); + } else if (readIf("LIKE")) { + c = factory.comparison(left, Operator.LIKE, parseStaticOperand()); + if (supportSQL1) { + if (readIf("ESCAPE")) { + StaticOperandImpl esc = parseStaticOperand(); + if (!(esc instanceof LiteralImpl)) { + throw getSyntaxError("only ESCAPE '\' is supported"); + } + PropertyValue v = ((LiteralImpl) esc).getLiteralValue(); + if (!v.getValue(Type.STRING).equals("\\")) { + throw getSyntaxError("only ESCAPE '\' is supported"); + } + } + } + } else if (readIf("IS")) { + boolean not = readIf("NOT"); + read("NULL"); + if (!(left instanceof PropertyValueImpl)) { + throw getSyntaxError("propertyName (NOT NULL is only supported for properties)"); + } + PropertyValueImpl p = (PropertyValueImpl) left; + c = getPropertyExistence(p); + if (!not) { + c = factory.not(c); + } + } else if (readIf("NOT")) { + if (readIf("IS")) { + read("NULL"); + if (!(left instanceof PropertyValueImpl)) { + throw new ParseException( + "Only property values can be tested for NOT IS NULL; got: " + + left.getClass().getName(), parseIndex); + } + PropertyValueImpl pv = (PropertyValueImpl) left; + c = getPropertyExistence(pv); + } else { + read("LIKE"); + c = factory.comparison(left, Operator.LIKE, parseStaticOperand()); + c = factory.not(c); + } + } else { + throw getSyntaxError(); + } + return c; + } + + private PropertyExistenceImpl getPropertyExistence(PropertyValueImpl p) throws ParseException { + return factory.propertyExistence(p.getSelectorName(), p.getPropertyName()); + } + + private ConstraintImpl parseConditionFunctionIf(String functionName) throws ParseException { + ConstraintImpl c; + if ("CONTAINS".equalsIgnoreCase(functionName)) { + if (readIf("*")) { + // strictly speaking, CONTAINS(*, ...) is not supported + // according to the spec: + // "If only one selector exists in this query, explicit + // specification of the selectorName preceding the + // propertyName is optional" + // but we anyway support it + read(","); + c = factory.fullTextSearch( + getOnlySelectorName(), null, parseStaticOperand()); + } else if (readIf(".")) { + if (!supportSQL1) { + throw getSyntaxError("selector name, property name, or *"); + } + read(","); + c = factory.fullTextSearch( + getOnlySelectorName(), null, parseStaticOperand()); + } else { + String name = readName(); + if (readIf(".")) { + if (readIf("*")) { + read(","); + c = factory.fullTextSearch( + name, null, parseStaticOperand()); + } else { + String selector = name; + name = readName(); + read(","); + c = factory.fullTextSearch( + selector, name, parseStaticOperand()); + } + } else { + read(","); + c = factory.fullTextSearch( + getOnlySelectorName(), name, + parseStaticOperand()); + } + } + } else if ("ISSAMENODE".equalsIgnoreCase(functionName)) { + String name = readName(); + if (readIf(",")) { + c = factory.sameNode(name, readAbsolutePath()); + } else { + c = factory.sameNode(getOnlySelectorName(), name); + } + } else if ("ISCHILDNODE".equalsIgnoreCase(functionName)) { + String name = readName(); + if (readIf(",")) { + c = factory.childNode(name, readAbsolutePath()); + } else { + c = factory.childNode(getOnlySelectorName(), name); + } + } else if ("ISDESCENDANTNODE".equalsIgnoreCase(functionName)) { + String name = readName(); + if (readIf(",")) { + c = factory.descendantNode(name, readAbsolutePath()); + } else { + c = factory.descendantNode(getOnlySelectorName(), name); + } + } else { + return null; + } + read(")"); + return c; + } + + private String readAbsolutePath() throws ParseException { + String path = readPath(); + if (!PathUtils.isAbsolute(path)) { + throw getSyntaxError("absolute path"); + } + return path; + } + + private String readPath() throws ParseException { + return readName(); + } + + private DynamicOperandImpl parseDynamicOperand() throws ParseException { + boolean identifier = currentTokenType == IDENTIFIER; + String name = readName(); + if (identifier && readIf("(")) { + return parseExpressionFunction(name); + } else { + return parsePropertyValue(name); + } + } + + private DynamicOperandImpl parseExpressionFunction(String functionName) throws ParseException { + DynamicOperandImpl op; + if ("LENGTH".equalsIgnoreCase(functionName)) { + op = factory.length(parsePropertyValue(readName())); + } else if ("NAME".equalsIgnoreCase(functionName)) { + if (isToken(")")) { + op = factory.nodeName(getOnlySelectorName()); + } else { + op = factory.nodeName(readName()); + } + } else if ("LOCALNAME".equalsIgnoreCase(functionName)) { + if (isToken(")")) { + op = factory.nodeLocalName(getOnlySelectorName()); + } else { + op = factory.nodeLocalName(readName()); + } + } else if ("SCORE".equalsIgnoreCase(functionName)) { + if (isToken(")")) { + op = factory.fullTextSearchScore(getOnlySelectorName()); + } else { + op = factory.fullTextSearchScore(readName()); + } + } else if ("LOWER".equalsIgnoreCase(functionName)) { + op = factory.lowerCase(parseDynamicOperand()); + } else if ("UPPER".equalsIgnoreCase(functionName)) { + op = factory.upperCase(parseDynamicOperand()); + } else if ("PROPERTY".equalsIgnoreCase(functionName)) { + PropertyValueImpl pv = parsePropertyValue(readName()); + read(","); + op = factory.propertyValue(pv.getSelectorName(), pv.getPropertyName(), readString().getValue(Type.STRING)); + } else { + throw getSyntaxError("LENGTH, NAME, LOCALNAME, SCORE, LOWER, UPPER, or PROPERTY"); + } + read(")"); + return op; + } + + private PropertyValueImpl parsePropertyValue(String name) throws ParseException { + if (readIf(".")) { + return factory.propertyValue(name, readName()); + } else { + return factory.propertyValue(getOnlySelectorName(), name); + } + } + + private StaticOperandImpl parseStaticOperand() throws ParseException { + if (currentTokenType == PLUS) { + read(); + } else if (currentTokenType == MINUS) { + read(); + if (currentTokenType != VALUE) { + throw getSyntaxError("number"); + } + int valueType = currentValue.getType().tag(); + switch (valueType) { + case PropertyType.LONG: + currentValue = PropertyValues.newLong(-currentValue.getValue(Type.LONG)); + break; + case PropertyType.DOUBLE: + currentValue = PropertyValues.newDouble(-currentValue.getValue(Type.DOUBLE)); + break; + case PropertyType.BOOLEAN: + currentValue = PropertyValues.newBoolean(!currentValue.getValue(Type.BOOLEAN)); + break; + case PropertyType.DECIMAL: + currentValue = PropertyValues.newDecimal(currentValue.getValue(Type.DECIMAL).negate()); + break; + default: + throw getSyntaxError("Illegal operation: -" + currentValue); + } + } + if (currentTokenType == VALUE) { + LiteralImpl literal = getUncastLiteral(currentValue); + read(); + return literal; + } else if (currentTokenType == PARAMETER) { + read(); + String name = readName(); + if (readIf(":")) { + name = name + ':' + readName(); + } + BindVariableValueImpl var = bindVariables.get(name); + if (var == null) { + var = factory.bindVariable(name); + bindVariables.put(name, var); + } + return var; + } else if (readIf("TRUE")) { + LiteralImpl literal = getUncastLiteral(PropertyValues.newBoolean(true)); + return literal; + } else if (readIf("FALSE")) { + LiteralImpl literal = getUncastLiteral(PropertyValues.newBoolean(false)); + return literal; + } else if (readIf("CAST")) { + read("("); + StaticOperandImpl op = parseStaticOperand(); + if (!(op instanceof LiteralImpl)) { + throw getSyntaxError("literal"); + } + LiteralImpl literal = (LiteralImpl) op; + PropertyValue value = literal.getLiteralValue(); + read("AS"); + value = parseCastAs(value); + read(")"); + // CastLiteral + literal = factory.literal(value); + return literal; + } else { + if (supportSQL1) { + if (readIf("TIMESTAMP")) { + StaticOperandImpl op = parseStaticOperand(); + if (!(op instanceof LiteralImpl)) { + throw getSyntaxError("literal"); + } + LiteralImpl literal = (LiteralImpl) op; + PropertyValue value = literal.getLiteralValue(); + value = PropertyValues.newDate(value.getValue(Type.STRING)); + literal = factory.literal(value); + return literal; + } + } + throw getSyntaxError("static operand"); + } + } + + /** + * Create a literal from a parsed value. + * + * @param value the original value + * @return the literal + */ + private LiteralImpl getUncastLiteral(PropertyValue value) { + return factory.literal(value); + } + + private PropertyValue parseCastAs(PropertyValue value) + throws ParseException { + if (currentTokenQuoted) { + throw getSyntaxError("data type (STRING|BINARY|...)"); + } + int propertyType = getPropertyTypeFromName(currentToken); + read(); + + PropertyValue v = PropertyValues.convert(value, propertyType, null); + if (v == null) { + throw getSyntaxError("data type (STRING|BINARY|...)"); + } + return v; + } + + /** + * Get the property type from the given case insensitive name. + * + * @param name the property type name (case insensitive) + * @return the type, or {@code PropertyType.UNDEFINED} if unknown + */ + public static int getPropertyTypeFromName(String name) { + if (matchesPropertyType(PropertyType.STRING, name)) { + return PropertyType.STRING; + } else if (matchesPropertyType(PropertyType.BINARY, name)) { + return PropertyType.BINARY; + } else if (matchesPropertyType(PropertyType.DATE, name)) { + return PropertyType.DATE; + } else if (matchesPropertyType(PropertyType.LONG, name)) { + return PropertyType.LONG; + } else if (matchesPropertyType(PropertyType.DOUBLE, name)) { + return PropertyType.DOUBLE; + } else if (matchesPropertyType(PropertyType.DECIMAL, name)) { + return PropertyType.DECIMAL; + } else if (matchesPropertyType(PropertyType.BOOLEAN, name)) { + return PropertyType.BOOLEAN; + } else if (matchesPropertyType(PropertyType.NAME, name)) { + return PropertyType.NAME; + } else if (matchesPropertyType(PropertyType.PATH, name)) { + return PropertyType.PATH; + } else if (matchesPropertyType(PropertyType.REFERENCE, name)) { + return PropertyType.REFERENCE; + } else if (matchesPropertyType(PropertyType.WEAKREFERENCE, name)) { + return PropertyType.WEAKREFERENCE; + } else if (matchesPropertyType(PropertyType.URI, name)) { + return PropertyType.URI; + } + return PropertyType.UNDEFINED; + } + + private static boolean matchesPropertyType(int propertyType, String name) { + String typeName = PropertyType.nameFromValue(propertyType); + return typeName.equalsIgnoreCase(name); + } + + private OrderingImpl[] parseOrder() throws ParseException { + ArrayList orderList = new ArrayList(); + do { + OrderingImpl ordering; + DynamicOperandImpl op = parseDynamicOperand(); + if (readIf("DESC")) { + ordering = factory.descending(op); + } else { + readIf("ASC"); + ordering = factory.ascending(op); + } + orderList.add(ordering); + } while (readIf(",")); + OrderingImpl[] orderings = new OrderingImpl[orderList.size()]; + orderList.toArray(orderings); + return orderings; + } + + private ArrayList parseColumns() throws ParseException { + ArrayList list = new ArrayList(); + if (readIf("*")) { + list.add(new ColumnOrWildcard()); + } else { + do { + ColumnOrWildcard column = new ColumnOrWildcard(); + column.propertyName = readName(); + if (readIf(".")) { + column.selectorName = column.propertyName; + if (readIf("*")) { + column.propertyName = null; + } else { + column.propertyName = readName(); + if (readIf("AS")) { + column.columnName = readName(); + } + } + } else { + if (readIf("AS")) { + column.columnName = readName(); + } + } + list.add(column); + } while (readIf(",")); + } + return list; + } + + private ColumnImpl[] resolveColumns(ArrayList list) throws ParseException { + ArrayList columns = new ArrayList(); + for (ColumnOrWildcard c : list) { + if (c.propertyName == null) { + for (SelectorImpl selector : selectors) { + if (c.selectorName == null + || c.selectorName + .equals(selector.getSelectorName())) { + ColumnImpl column = factory.column(selector + .getSelectorName(), null, null); + columns.add(column); + } + } + } else { + ColumnImpl column; + if (c.selectorName != null) { + column = factory.column(c.selectorName, c.propertyName, c.columnName); + } else if (c.columnName != null) { + column = factory.column(getOnlySelectorName(), c.propertyName, c.columnName); + } else { + column = factory.column(getOnlySelectorName(), c.propertyName, c.propertyName); + } + columns.add(column); + } + } + ColumnImpl[] array = new ColumnImpl[columns.size()]; + columns.toArray(array); + return array; + } + + private boolean readIf(String token) throws ParseException { + if (isToken(token)) { + read(); + return true; + } + return false; + } + + private boolean isToken(String token) { + boolean result = token.equalsIgnoreCase(currentToken) && !currentTokenQuoted; + if (result) { + return true; + } + addExpected(token); + return false; + } + + private void read(String expected) throws ParseException { + if (!expected.equalsIgnoreCase(currentToken) || currentTokenQuoted) { + throw getSyntaxError(expected); + } + read(); + } + + private String readAny() throws ParseException { + if (currentTokenType == END) { + throw getSyntaxError("a token"); + } + String s; + if (currentTokenType == VALUE) { + s = currentValue.getValue(Type.STRING); + } else { + s = currentToken; + } + read(); + return s; + } + + private PropertyValue readString() throws ParseException { + if (currentTokenType != VALUE) { + throw getSyntaxError("string value"); + } + PropertyValue value = currentValue; + read(); + return value; + } + + private void addExpected(String token) { + if (expected != null) { + expected.add(token); + } + } + + private void initialize(String query) throws ParseException { + if (query == null) { + query = ""; + } + statement = query; + int len = query.length() + 1; + char[] command = new char[len]; + int[] types = new int[len]; + len--; + query.getChars(0, len, command, 0); + command[len] = ' '; + int startLoop = 0; + for (int i = 0; i < len; i++) { + char c = command[i]; + int type = 0; + switch (c) { + case '/': + case '-': + case '(': + case ')': + case '{': + case '}': + case '*': + case ',': + case ';': + case '+': + case '%': + case '?': + case '$': + case '[': + case ']': + type = CHAR_SPECIAL_1; + break; + case '!': + case '<': + case '>': + case '|': + case '=': + case ':': + type = CHAR_SPECIAL_2; + break; + case '.': + type = CHAR_DECIMAL; + break; + case '\'': + type = CHAR_STRING; + types[i] = CHAR_STRING; + startLoop = i; + while (command[++i] != '\'') { + checkRunOver(i, len, startLoop); + } + break; + case '\"': + type = CHAR_QUOTED; + types[i] = CHAR_QUOTED; + startLoop = i; + while (command[++i] != '\"') { + checkRunOver(i, len, startLoop); + } + break; + case '_': + type = CHAR_NAME; + break; + default: + if (c >= 'a' && c <= 'z') { + type = CHAR_NAME; + } else if (c >= 'A' && c <= 'Z') { + type = CHAR_NAME; + } else if (c >= '0' && c <= '9') { + type = CHAR_VALUE; + } else { + if (Character.isJavaIdentifierPart(c)) { + type = CHAR_NAME; + } + } + } + types[i] = (byte) type; + } + statementChars = command; + types[len] = CHAR_END; + characterTypes = types; + parseIndex = 0; + } + + private void checkRunOver(int i, int len, int startLoop) throws ParseException { + if (i >= len) { + parseIndex = startLoop; + throw getSyntaxError(); + } + } + + private void read() throws ParseException { + currentTokenQuoted = false; + if (expected != null) { + expected.clear(); + } + int[] types = characterTypes; + int i = parseIndex; + int type = types[i]; + while (type == 0) { + type = types[++i]; + } + int start = i; + char[] chars = statementChars; + char c = chars[i++]; + currentToken = ""; + switch (type) { + case CHAR_NAME: + while (true) { + type = types[i]; + if (type != CHAR_NAME && type != CHAR_VALUE) { + c = chars[i]; + if (supportSQL1 && c == ':') { + i++; + continue; + } + break; + } + i++; + } + currentToken = statement.substring(start, i); + if (currentToken.isEmpty()) { + throw getSyntaxError(); + } + currentTokenType = IDENTIFIER; + parseIndex = i; + return; + case CHAR_SPECIAL_2: + if (types[i] == CHAR_SPECIAL_2) { + i++; + } + // fall through + case CHAR_SPECIAL_1: + currentToken = statement.substring(start, i); + switch (c) { + case '$': + currentTokenType = PARAMETER; + break; + case '+': + currentTokenType = PLUS; + break; + case '-': + currentTokenType = MINUS; + break; + case '(': + currentTokenType = OPEN; + break; + case ')': + currentTokenType = CLOSE; + break; + default: + currentTokenType = KEYWORD; + } + parseIndex = i; + return; + case CHAR_VALUE: + long number = c - '0'; + while (true) { + c = chars[i]; + if (c < '0' || c > '9') { + if (c == '.') { + readDecimal(start, i); + break; + } + if (c == 'E' || c == 'e') { + readDecimal(start, i); + break; + } + checkLiterals(false); + currentValue = PropertyValues.newLong(number); + currentTokenType = VALUE; + currentToken = "0"; + parseIndex = i; + break; + } + number = number * 10 + (c - '0'); + if (number > Integer.MAX_VALUE) { + readDecimal(start, i); + break; + } + i++; + } + return; + case CHAR_DECIMAL: + if (types[i] != CHAR_VALUE) { + currentTokenType = KEYWORD; + currentToken = "."; + parseIndex = i; + return; + } + readDecimal(i - 1, i); + return; + case CHAR_STRING: + readString(i, '\''); + return; + case CHAR_QUOTED: + readString(i, '\"'); + if (supportSQL1) { + // for SQL-2, this is a literal, as defined in + // the JCR 2.0 spec, 6.7.34 Literal - UncastLiteral + // but for compatibility with Jackrabbit 2.x, for + // SQL-1, this is an identifier, as in ANSI SQL + // (not in the JCR 1.0 spec) + // (confusing isn't it?) + currentTokenType = IDENTIFIER; + currentToken = currentValue.getValue(Type.STRING); + } + return; + case CHAR_END: + currentToken = ""; + currentTokenType = END; + parseIndex = i; + return; + default: + throw getSyntaxError(); + } + } + + private void readString(int i, char end) throws ParseException { + char[] chars = statementChars; + String result = null; + while (true) { + for (int begin = i;; i++) { + if (chars[i] == end) { + if (result == null) { + result = statement.substring(begin, i); + } else { + result += statement.substring(begin - 1, i); + } + break; + } + } + if (chars[++i] != end) { + break; + } + i++; + } + currentToken = "'"; + checkLiterals(false); + currentValue = PropertyValues.newString(result); + parseIndex = i; + currentTokenType = VALUE; + } + + private void checkLiterals(boolean text) throws ParseException { + if (text && !allowTextLiterals || !text && !allowNumberLiterals) { + throw getSyntaxError("bind variable (literals of this type not allowed)"); + } + } + + private void readDecimal(int start, int i) throws ParseException { + char[] chars = statementChars; + int[] types = characterTypes; + while (true) { + int t = types[i]; + if (t != CHAR_DECIMAL && t != CHAR_VALUE) { + break; + } + i++; + } + if (chars[i] == 'E' || chars[i] == 'e') { + i++; + if (chars[i] == '+' || chars[i] == '-') { + i++; + } + if (types[i] != CHAR_VALUE) { + throw getSyntaxError(); + } + while (types[++i] == CHAR_VALUE) { + // go until the first non-number + } + } + parseIndex = i; + String sub = statement.substring(start, i); + BigDecimal bd; + try { + bd = new BigDecimal(sub); + } catch (NumberFormatException e) { + throw new ParseException("Data conversion error converting " + sub + " to BigDecimal: " + e, parseIndex); + } + checkLiterals(false); + + currentValue = PropertyValues.newDecimal(bd); + currentTokenType = VALUE; + } + + private ParseException getSyntaxError() { + if (expected == null || expected.isEmpty()) { + return getSyntaxError(null); + } else { + StringBuilder buff = new StringBuilder(); + for (String exp : expected) { + if (buff.length() > 0) { + buff.append(", "); + } + buff.append(exp); + } + return getSyntaxError(buff.toString()); + } + } + + private ParseException getSyntaxError(String expected) { + int index = Math.max(0, Math.min(parseIndex, statement.length() - 1)); + String query = statement.substring(0, index) + "(*)" + statement.substring(index).trim(); + if (expected != null) { + query += "; expected: " + expected; + } + return new ParseException("Query: " + query, index); + } + + /** + * Represents a column or a wildcard in a SQL expression. + * This class is temporarily used during parsing. + */ + static class ColumnOrWildcard { + String selectorName; + String propertyName; + String columnName; + } + + /** + * Get the selector name if only one selector exists in the query. + * If more than one selector exists, an exception is thrown. + * + * @return the selector name + */ + private String getOnlySelectorName() throws ParseException { + if (selectors.size() > 1) { + throw getSyntaxError("Need to specify the selector name because the query contains more than one selector."); + } + return selectors.get(0).getSelectorName(); + } + + public static String escapeStringLiteral(String value) { + return '\'' + value.replace("'", "''") + '\''; + } + + /** + * Enable or disable support for text literals in queries. The default is enabled. + * + * @param allowTextLiterals + */ + public void setAllowTextLiterals(boolean allowTextLiterals) { + this.allowTextLiterals = allowTextLiterals; + } + + public void setAllowNumberLiterals(boolean allowNumberLiterals) { + this.allowNumberLiterals = allowNumberLiterals; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/SessionQueryEngineImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/SessionQueryEngineImpl.java new file mode 100644 index 00000000000..8e0c9f8cbe1 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/SessionQueryEngineImpl.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.query; + +import java.text.ParseException; +import java.util.List; +import java.util.Map; + +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.SessionQueryEngine; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * The query engine implementation bound to a session. + */ +public abstract class SessionQueryEngineImpl implements SessionQueryEngine { + + private final QueryIndexProvider indexProvider; + + public SessionQueryEngineImpl(QueryIndexProvider indexProvider) { + this.indexProvider = indexProvider; + } + + /** + * The implementing class must return the current root {@link NodeState} + * associated with the {@link ContentSession}. + * + * @return the current root {@link NodeState}. + */ + protected abstract NodeState getRootNodeState(); + + /** + * The implementing class must return the root associated with the + * {@link ContentSession}. + * + * @return the root associated with the {@link ContentSession}. + */ + protected abstract Root getRoot(); + + @Override + public List getSupportedQueryLanguages() { + return createQueryEngine().getSupportedQueryLanguages(); + } + + @Override + public List getBindVariableNames(String statement, String language) + throws ParseException { + return createQueryEngine().getBindVariableNames(statement, language); + } + + @Override + public Result executeQuery(String statement, String language, long limit, + long offset, Map bindings, + NamePathMapper namePathMapper) throws ParseException { + return createQueryEngine().executeQuery(statement, language, limit, + offset, bindings, getRoot(), namePathMapper); + } + + private QueryEngineImpl createQueryEngine() { + return new QueryEngineImpl(getRootNodeState(), indexProvider); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/XPathToSQL2Converter.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/XPathToSQL2Converter.java new file mode 100644 index 00000000000..c9b2a0ff3c7 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/XPathToSQL2Converter.java @@ -0,0 +1,1288 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.query; + +import org.apache.jackrabbit.oak.commons.PathUtils; + +import java.math.BigDecimal; +import java.text.ParseException; +import java.util.ArrayList; + +/** + * This class can can convert a XPATH query to a SQL2 query. + */ +public class XPathToSQL2Converter { + + // Character types, used during the tokenizer phase + private static final int CHAR_END = -1, CHAR_VALUE = 2; + private static final int CHAR_NAME = 4, CHAR_SPECIAL_1 = 5, CHAR_SPECIAL_2 = 6; + private static final int CHAR_STRING = 7, CHAR_DECIMAL = 8; + + // Token types + private static final int KEYWORD = 1, IDENTIFIER = 2, END = 4, VALUE_STRING = 5, VALUE_NUMBER = 6; + private static final int MINUS = 12, PLUS = 13, OPEN = 14, CLOSE = 15; + + // The query as an array of characters and character types + private String statement; + private char[] statementChars; + private int[] characterTypes; + + // The current state of the parser + private int parseIndex; + private int currentTokenType; + private String currentToken; + private boolean currentTokenQuoted; + private ArrayList expected; + private Selector currentSelector = new Selector(); + private ArrayList selectors = new ArrayList(); + + /** + * Convert the query to SQL2. + * + * @param query the query string + * @return the SQL2 query + * @throws ParseException if parsing fails + */ + public String convert(String query) throws ParseException { + query = query.trim(); + boolean explain = query.startsWith("explain "); + if (explain) { + query = query.substring("explain".length()).trim(); + } + boolean measure = query.startsWith("measure"); + if (measure) { + query = query.substring("measure".length()).trim(); + } + + if (query.isEmpty()) { + // special case, will always result in an empty result + query = "//jcr:root"; + } + + initialize(query); + + expected = new ArrayList(); + read(); + + if (currentTokenType == END) { + throw getSyntaxError("the query may not be empty"); + } + + currentSelector.name = "a"; + + ArrayList columnList = new ArrayList(); + + // TODO support "..", example: + // /jcr:root/etc/.. + + String pathPattern = ""; + boolean startOfQuery = true; + + while (true) { + + // if true, path or nodeType conditions are not allowed + boolean shortcut = false; + boolean slash = readIf("/"); + + if (!slash) { + if (startOfQuery) { + // the query doesn't start with "/" + currentSelector.path = "/"; + pathPattern = "/"; + currentSelector.isChild = true; + } else { + break; + } + } else if (readIf("jcr:root")) { + // "/jcr:root" may only appear at the beginning + if (!pathPattern.isEmpty()) { + throw getSyntaxError("jcr:root needs to be at the beginning"); + } + if (readIf("/")) { + // "/jcr:root/" + currentSelector.path = "/"; + pathPattern = "/"; + if (readIf("/")) { + // "/jcr:root//" + pathPattern = "//"; + currentSelector.isDescendant = true; + } else { + currentSelector.isChild = true; + } + } else { + // for example "/jcr:root[condition]" + pathPattern = "/%"; + currentSelector.path = "/"; + shortcut = true; + } + } else if (readIf("/")) { + // "//" was read + pathPattern += "%"; + currentSelector.isDescendant = true; + } else { + // the token "/" was read + pathPattern += "/"; + if (startOfQuery) { + currentSelector.path = "/"; + } else { + currentSelector.isChild = true; + } + } + if (shortcut) { + // "*" and so on are not allowed now + } else if (readIf("*")) { + // "...*" + pathPattern += "%"; + if (!currentSelector.isDescendant) { + if (selectors.size() == 0 && currentSelector.path.equals("")) { + // the query /* is special + currentSelector.path = "/"; + } + } + } else if (readIf("text")) { + // "...text()" + currentSelector.isChild = false; + pathPattern += "jcr:xmltext"; + read("("); + read(")"); + if (currentSelector.isDescendant) { + currentSelector.nodeName = "jcr:xmltext"; + } else { + currentSelector.path = PathUtils.concat(currentSelector.path, "jcr:xmltext"); + } + } else if (readIf("element")) { + // "...element(..." + read("("); + if (readIf(")")) { + // any + pathPattern += "%"; + } else { + if (readIf("*")) { + // any + pathPattern += "%"; + } else { + currentSelector.isChild = false; + String name = readIdentifier(); + pathPattern += name; + currentSelector.path = PathUtils.concat(currentSelector.path, name); + } + if (readIf(",")) { + currentSelector.nodeType = readIdentifier(); + } + read(")"); + } + } else if (readIf("@")) { + Property p = readProperty(); + columnList.add(p); + } else if (readIf("(")) { + // special case: ".../(@prop)" is actually not a child node, + // but the same node (selector) as before + if (selectors.size() > 0) { + currentSelector = selectors.remove(selectors.size() - 1); + // prevent (join) conditions are added again + currentSelector.isChild = false; + currentSelector.isDescendant = false; + currentSelector.path = ""; + currentSelector.nodeName = null; + } + do { + read("@"); + Property p = readProperty(); + columnList.add(p); + } while (readIf("|")); + read(")"); + } else if (currentTokenType == IDENTIFIER) { + // path restriction + String name = readIdentifier(); + pathPattern += name; + if (!currentSelector.isChild) { + currentSelector.nodeName = name; + } else { + if (selectors.size() > 0) { + // no explicit path restriction - so it's a node name restriction + currentSelector.isChild = true; + currentSelector.nodeName = name; + } else { + if (currentSelector.isChild) { + currentSelector.isChild = false; + String oldPath = currentSelector.path; + // further extending the path + currentSelector.path = PathUtils.concat(oldPath, name); + } + } + } + } else { + throw getSyntaxError(); + } + if (readIf("[")) { + Expression c = parseConstraint(); + currentSelector.condition = add(currentSelector.condition, c); + read("]"); + } + startOfQuery = false; + nextSelector(false); + } + if (selectors.size() == 0) { + nextSelector(true); + } + // the current selector wasn't used so far + // go back to the last one + currentSelector = selectors.get(selectors.size() - 1); + if (selectors.size() == 1) { + currentSelector.onlySelector = true; + } + ArrayList orderList = new ArrayList(); + if (readIf("order")) { + read("by"); + do { + Order order = new Order(); + order.expr = parseExpression(); + if (readIf("descending")) { + order.descending = true; + } else { + readIf("ascending"); + } + orderList.add(order); + } while (readIf(",")); + } + if (!currentToken.isEmpty()) { + throw getSyntaxError(""); + } + StringBuilder buff = new StringBuilder(); + + // explain | measure ... + if (explain) { + buff.append("explain "); + } else if (measure) { + buff.append("measure "); + } + + // select ... + buff.append("select "); + buff.append(new Property(currentSelector, Query.JCR_PATH).toString()); + if (selectors.size() > 1) { + buff.append(" as ").append('[').append(Query.JCR_PATH).append(']'); + } + buff.append(", "); + buff.append(new Property(currentSelector, Query.JCR_SCORE).toString()); + if (selectors.size() > 1) { + buff.append(" as ").append('[').append(Query.JCR_SCORE).append(']'); + } + if (columnList.isEmpty()) { + buff.append(", "); + buff.append(new Property(currentSelector, "*").toString()); + } else { + for (int i = 0; i < columnList.size(); i++) { + buff.append(", "); + Expression e = columnList.get(i); + String columnName = e.toString(); + buff.append(columnName); + if (selectors.size() > 1) { + buff.append(" as [").append(e.getColumnAliasName()).append("]"); + } + } + } + + // from ... + buff.append(" from "); + for (int i = 0; i < selectors.size(); i++) { + Selector s = selectors.get(i); + if (i > 0) { + buff.append(" inner join "); + } + String nodeType = s.nodeType; + if (nodeType == null) { + nodeType = "nt:base"; + } + buff.append('[' + nodeType + ']').append(" as ").append(s.name); + if (s.joinCondition != null) { + buff.append(" on ").append(s.joinCondition); + } + } + + // where ... + StringBuilder condition = new StringBuilder(); + for (int i = 0; i < selectors.size(); i++) { + Selector s = selectors.get(i); + if (s.condition != null) { + if (condition.length() > 0) { + condition.append(" and "); + } + condition.append(s.condition); + } + } + if (condition.length() > 0) { + buff.append(" where ").append(condition.toString()); + } + + // order by ... + if (!orderList.isEmpty()) { + buff.append(" order by "); + for (int i = 0; i < orderList.size(); i++) { + if (i > 0) { + buff.append(", "); + } + buff.append(orderList.get(i)); + } + } + return buff.toString(); + } + + private void nextSelector(boolean force) throws ParseException { + boolean isFirstSelector = selectors.size() == 0; + String path = currentSelector.path; + Expression condition = currentSelector.condition; + Expression joinCondition = currentSelector.joinCondition; + if (currentSelector.nodeName != null) { + Function f = new Function("name"); + f.params.add(new SelectorExpr(currentSelector)); + Condition c = new Condition(f, "=", + Literal.newString(currentSelector.nodeName), + Expression.PRECEDENCE_CONDITION); + condition = add(condition, c); + } + if (currentSelector.isDescendant) { + if (isFirstSelector) { + if (!path.isEmpty()) { + if (!PathUtils.isAbsolute(path)) { + path = PathUtils.concat("/", path); + } + Function c = new Function("isdescendantnode"); + c.params.add(new SelectorExpr(currentSelector)); + c.params.add(Literal.newString(path)); + condition = add(condition, c); + } + } else { + Function c = new Function("isdescendantnode"); + c.params.add(new SelectorExpr(currentSelector)); + c.params.add(new SelectorExpr(selectors.get(selectors.size() - 1))); + joinCondition = c; + } + } else if (currentSelector.isChild) { + if (isFirstSelector) { + if (!path.isEmpty()) { + if (!PathUtils.isAbsolute(path)) { + path = PathUtils.concat("/", path); + } + Function c = new Function("ischildnode"); + c.params.add(new SelectorExpr(currentSelector)); + c.params.add(Literal.newString(path)); + condition = add(condition, c); + } + } else { + Function c = new Function("ischildnode"); + c.params.add(new SelectorExpr(currentSelector)); + c.params.add(new SelectorExpr(selectors.get(selectors.size() - 1))); + joinCondition = c; + } + } else { + if (!force && condition == null && joinCondition == null) { + // a child node of a given path, such as "/test" + // use the same selector for now, and extend the path + } else if (PathUtils.isAbsolute(path)) { + Function c = new Function("issamenode"); + c.params.add(new SelectorExpr(currentSelector)); + c.params.add(Literal.newString(path)); + condition = add(condition, c); + } + } + if (force || condition != null || joinCondition != null) { + String nextSelectorName = "" + (char) (currentSelector.name.charAt(0) + 1); + if (nextSelectorName.compareTo("x") > 0) { + throw getSyntaxError("too many joins"); + } + Selector nextSelector = new Selector(); + nextSelector.name = nextSelectorName; + currentSelector.condition = condition; + currentSelector.joinCondition = joinCondition; + selectors.add(currentSelector); + currentSelector = nextSelector; + } + } + + private static Expression add(Expression old, Expression add) { + if (old == null) { + return add; + } + return new Condition(old, "and", add, Expression.PRECEDENCE_AND); + } + + private Expression parseConstraint() throws ParseException { + Expression a = parseAnd(); + while (readIf("or")) { + a = new Condition(a, "or", parseAnd(), Expression.PRECEDENCE_OR); + } + return a; + } + + private Expression parseAnd() throws ParseException { + Expression a = parseCondition(); + while (readIf("and")) { + a = new Condition(a, "and", parseCondition(), Expression.PRECEDENCE_AND); + } + return a; + } + + private Expression parseCondition() throws ParseException { + Expression a; + if (readIf("fn:not") || readIf("not")) { + read("("); + a = parseConstraint(); + if (a instanceof Condition && ((Condition) a).operator.equals("is not null")) { + // not(@property) -> @property is null + Condition c = (Condition) a; + c = new Condition(c.left, "is null", null, Expression.PRECEDENCE_CONDITION); + a = c; + } else { + Function f = new Function("not"); + f.params.add(a); + a = f; + } + read(")"); + } else if (readIf("(")) { + a = parseConstraint(); + read(")"); + } else { + Expression e = parseExpression(); + if (e.isCondition()) { + return e; + } + a = parseCondition(e); + } + return a; + } + + private Condition parseCondition(Expression left) throws ParseException { + Condition c; + if (readIf("=")) { + c = new Condition(left, "=", parseExpression(), Expression.PRECEDENCE_CONDITION); + } else if (readIf("<>")) { + c = new Condition(left, "<>", parseExpression(), Expression.PRECEDENCE_CONDITION); + } else if (readIf("!=")) { + c = new Condition(left, "<>", parseExpression(), Expression.PRECEDENCE_CONDITION); + } else if (readIf("<")) { + c = new Condition(left, "<", parseExpression(), Expression.PRECEDENCE_CONDITION); + } else if (readIf(">")) { + c = new Condition(left, ">", parseExpression(), Expression.PRECEDENCE_CONDITION); + } else if (readIf("<=")) { + c = new Condition(left, "<=", parseExpression(), Expression.PRECEDENCE_CONDITION); + } else if (readIf(">=")) { + c = new Condition(left, ">=", parseExpression(), Expression.PRECEDENCE_CONDITION); + // TODO support "x eq y"? it seems this only matches for single value properties? + // } else if (readIf("eq")) { + // c = new Condition(left, "==", parseExpression(), Expression.PRECEDENCE_CONDITION); + } else { + c = new Condition(left, "is not null", null, Expression.PRECEDENCE_CONDITION); + } + return c; + } + + private Expression parseExpression() throws ParseException { + if (readIf("@")) { + return readProperty(); + } else if (readIf("true")) { + return Literal.newBoolean(true); + } else if (readIf("false")) { + return Literal.newBoolean(false); + } else if (currentTokenType == VALUE_NUMBER) { + Literal l = Literal.newNumber(currentToken); + read(); + return l; + } else if (currentTokenType == VALUE_STRING) { + Literal l = Literal.newString(currentToken); + read(); + return l; + } else if (readIf("-")) { + if (currentTokenType != VALUE_NUMBER) { + throw getSyntaxError(); + } + Literal l = Literal.newNumber('-' + currentToken); + read(); + return l; + } else if (readIf("+")) { + if (currentTokenType != VALUE_NUMBER) { + throw getSyntaxError(); + } + return parseExpression(); + } else { + return parsePropertyOrFunction(); + } + } + + private Expression parsePropertyOrFunction() throws ParseException { + StringBuilder buff = new StringBuilder(); + boolean isPath = false; + while (true) { + if (currentTokenType == IDENTIFIER) { + String name = readIdentifier(); + buff.append(name); + } else if (readIf("*")) { + // any node + buff.append('*'); + isPath = true; + } else if (readIf(".")) { + buff.append('.'); + if (readIf(".")) { + buff.append('.'); + } + isPath = true; + } else if (readIf("@")) { + if (readIf("*")) { + // xpath supports @*, even thought jackrabbit may not + buff.append('*'); + } else { + buff.append(readIdentifier()); + } + return new Property(currentSelector, buff.toString()); + } else { + break; + } + if (readIf("/")) { + isPath = true; + buff.append('/'); + } else { + break; + } + } + if (!isPath && readIf("(")) { + return parseFunction(buff.toString()); + } else if (buff.length() > 0) { + // path without all attributes, as in: + // jcr:contains(jcr:content, 'x') + if (buff.toString().equals(".")) { + buff = new StringBuilder("*"); + } else { + buff.append("/*"); + } + return new Property(currentSelector, buff.toString()); + } + throw getSyntaxError(); + } + + private Expression parseFunction(String functionName) throws ParseException { + if ("jcr:like".equals(functionName)) { + Condition c = new Condition(parseExpression(), + "like", null, Expression.PRECEDENCE_CONDITION); + read(","); + c.right = parseExpression(); + read(")"); + return c; + } else if ("jcr:contains".equals(functionName)) { + Function f = new Function("contains"); + f.params.add(parseExpression()); + read(","); + f.params.add(parseExpression()); + read(")"); + return f; + } else if ("jcr:score".equals(functionName)) { + Function f = new Function("score"); + f.params.add(new SelectorExpr(currentSelector)); + read(")"); + return f; + } else if ("xs:dateTime".equals(functionName)) { + Expression expr = parseExpression(); + Cast c = new Cast(expr, "date"); + read(")"); + return c; + } else if ("fn:lower-case".equals(functionName)) { + Function f = new Function("lower"); + f.params.add(parseExpression()); + read(")"); + return f; + } else if ("fn:upper-case".equals(functionName)) { + Function f = new Function("upper"); + f.params.add(parseExpression()); + read(")"); + return f; + } else if ("fn:name".equals(functionName)) { + Function f = new Function("name"); + if (!readIf(")")) { + // only name(.) and name() are currently supported + read("."); + read(")"); + } + f.params.add(new SelectorExpr(currentSelector)); + return f; + } else if ("jcr:deref".equals(functionName)) { + // TODO maybe support jcr:deref + throw getSyntaxError("jcr:deref is not supported"); + } else if ("rep:similar".equals(functionName)) { + // TODO maybe support rep:similar + throw getSyntaxError("rep:similar is not supported"); + } else if ("rep:spellcheck".equals(functionName)) { + // TODO maybe support rep:spellcheck as in + // /jcr:root[rep:spellcheck('${query}')]/(rep:spellcheck()) + throw getSyntaxError("rep:spellcheck is not supported"); + } else { + throw getSyntaxError("jcr:like | jcr:contains | jcr:score | xs:dateTime | " + + "fn:lower-case | fn:upper-case | fn:name"); + } + } + + private boolean readIf(String token) throws ParseException { + if (isToken(token)) { + read(); + return true; + } + return false; + } + + private boolean isToken(String token) { + boolean result = token.equals(currentToken) && !currentTokenQuoted; + if (result) { + return true; + } + addExpected(token); + return false; + } + + private void read(String expected) throws ParseException { + if (!expected.equals(currentToken) || currentTokenQuoted) { + throw getSyntaxError(expected); + } + read(); + } + + private Property readProperty() throws ParseException { + if (readIf("*")) { + return new Property(currentSelector, "*"); + } + return new Property(currentSelector, readIdentifier()); + } + + private String readIdentifier() throws ParseException { + if (currentTokenType != IDENTIFIER) { + throw getSyntaxError("identifier"); + } + String s = currentToken; + read(); + return s; + } + + private void addExpected(String token) { + if (expected != null) { + expected.add(token); + } + } + + private void initialize(String query) throws ParseException { + if (query == null) { + query = ""; + } + statement = query; + int len = query.length() + 1; + char[] command = new char[len]; + int[] types = new int[len]; + len--; + query.getChars(0, len, command, 0); + command[len] = ' '; + int startLoop = 0; + for (int i = 0; i < len; i++) { + char c = command[i]; + int type = 0; + switch (c) { + case '@': + case '|': + case '/': + case '-': + case '(': + case ')': + case '{': + case '}': + case '*': + case ',': + case ';': + case '+': + case '%': + case '?': + case '$': + case '[': + case ']': + type = CHAR_SPECIAL_1; + break; + case '!': + case '<': + case '>': + case '=': + type = CHAR_SPECIAL_2; + break; + case '.': + type = CHAR_DECIMAL; + break; + case '\'': + type = CHAR_STRING; + types[i] = CHAR_STRING; + startLoop = i; + while (command[++i] != '\'') { + checkRunOver(i, len, startLoop); + } + break; + case '\"': + type = CHAR_STRING; + types[i] = CHAR_STRING; + startLoop = i; + while (command[++i] != '\"') { + checkRunOver(i, len, startLoop); + } + break; + case ':': + case '_': + type = CHAR_NAME; + break; + default: + if (c >= 'a' && c <= 'z') { + type = CHAR_NAME; + } else if (c >= 'A' && c <= 'Z') { + type = CHAR_NAME; + } else if (c >= '0' && c <= '9') { + type = CHAR_VALUE; + } else { + if (Character.isJavaIdentifierPart(c)) { + type = CHAR_NAME; + } + } + } + types[i] = (byte) type; + } + statementChars = command; + types[len] = CHAR_END; + characterTypes = types; + parseIndex = 0; + } + + private void checkRunOver(int i, int len, int startLoop) throws ParseException { + if (i >= len) { + parseIndex = startLoop; + throw getSyntaxError(); + } + } + + private void read() throws ParseException { + currentTokenQuoted = false; + if (expected != null) { + expected.clear(); + } + int[] types = characterTypes; + int i = parseIndex; + int type = types[i]; + while (type == 0) { + type = types[++i]; + } + int start = i; + char[] chars = statementChars; + char c = chars[i++]; + currentToken = ""; + switch (type) { + case CHAR_NAME: + while (true) { + type = types[i]; + // the '-' can be part of a name, + // for example in "fn:lower-case" + if (type != CHAR_NAME && type != CHAR_VALUE && chars[i] != '-') { + c = chars[i]; + break; + } + i++; + } + currentToken = statement.substring(start, i); + if (currentToken.isEmpty()) { + throw getSyntaxError(); + } + currentTokenType = IDENTIFIER; + parseIndex = i; + return; + case CHAR_SPECIAL_2: + if (types[i] == CHAR_SPECIAL_2) { + i++; + } + // fall through + case CHAR_SPECIAL_1: + currentToken = statement.substring(start, i); + switch (c) { + case '+': + currentTokenType = PLUS; + break; + case '-': + currentTokenType = MINUS; + break; + case '(': + currentTokenType = OPEN; + break; + case ')': + currentTokenType = CLOSE; + break; + default: + currentTokenType = KEYWORD; + } + parseIndex = i; + return; + case CHAR_VALUE: + long number = c - '0'; + while (true) { + c = chars[i]; + if (c < '0' || c > '9') { + if (c == '.') { + readDecimal(start, i); + break; + } + if (c == 'E' || c == 'e') { + readDecimal(start, i); + break; + } + currentTokenType = VALUE_NUMBER; + currentToken = String.valueOf(number); + parseIndex = i; + break; + } + number = number * 10 + (c - '0'); + if (number > Integer.MAX_VALUE) { + readDecimal(start, i); + break; + } + i++; + } + return; + case CHAR_DECIMAL: + if (types[i] != CHAR_VALUE) { + currentTokenType = KEYWORD; + currentToken = "."; + parseIndex = i; + return; + } + readDecimal(i - 1, i); + return; + case CHAR_STRING: + if (chars[i - 1] == '\'') { + readString(i, '\''); + } else { + readString(i, '\"'); + } + return; + case CHAR_END: + currentToken = ""; + currentTokenType = END; + parseIndex = i; + return; + default: + throw getSyntaxError(); + } + } + + private void readString(int i, char end) throws ParseException { + char[] chars = statementChars; + String result = null; + while (true) { + for (int begin = i;; i++) { + if (chars[i] == end) { + if (result == null) { + result = statement.substring(begin, i); + } else { + result += statement.substring(begin - 1, i); + } + break; + } + } + if (chars[++i] != end) { + break; + } + i++; + } + currentToken = result; + parseIndex = i; + currentTokenType = VALUE_STRING; + } + + private void readDecimal(int start, int i) throws ParseException { + char[] chars = statementChars; + int[] types = characterTypes; + while (true) { + int t = types[i]; + if (t != CHAR_DECIMAL && t != CHAR_VALUE) { + break; + } + i++; + } + if (chars[i] == 'E' || chars[i] == 'e') { + i++; + if (chars[i] == '+' || chars[i] == '-') { + i++; + } + if (types[i] != CHAR_VALUE) { + throw getSyntaxError(); + } + while (types[++i] == CHAR_VALUE) { + // go until the first non-number + } + } + parseIndex = i; + String sub = statement.substring(start, i); + try { + new BigDecimal(sub); + } catch (NumberFormatException e) { + throw new ParseException("Data conversion error converting " + sub + " to BigDecimal: " + e, i); + } + currentToken = sub; + currentTokenType = VALUE_NUMBER; + } + + private ParseException getSyntaxError() { + if (expected == null || expected.isEmpty()) { + return getSyntaxError(null); + } else { + StringBuilder buff = new StringBuilder(); + for (String exp : expected) { + if (buff.length() > 0) { + buff.append(", "); + } + buff.append(exp); + } + return getSyntaxError(buff.toString()); + } + } + + private ParseException getSyntaxError(String expected) { + int index = Math.max(0, Math.min(parseIndex, statement.length() - 1)); + String query = statement.substring(0, index) + "(*)" + statement.substring(index).trim(); + if (expected != null) { + query += "; expected: " + expected; + } + return new ParseException("Query:\n" + query, index); + } + + /** + * A selector. + */ + static class Selector { + + /** + * The selector name. + */ + String name; + + /** + * Whether this is the only selector in the query. + */ + boolean onlySelector; + + /** + * The node type, if set, or null. + */ + String nodeType; + + /** + * Whether this is a child node of the previous selector or a given path. + */ + // queries of the type + // jcr:root/* + // jcr:root/test/* + // jcr:root/element() + // jcr:root/element(*) + boolean isChild; + + /** + * Whether this is a descendant of the previous selector or a given path. + */ + // queries of the type + // /jcr:root//... + // /jcr:root/test//... + // /jcr:root[...] + // /jcr:root (just by itself) + boolean isDescendant; + + /** + * The path (only used for the first selector). + */ + String path = ""; + + /** + * The node name, if set. + */ + String nodeName; + + /** + * The condition for this selector. + */ + Expression condition; + + /** + * The join condition from the previous selector. + */ + Expression joinCondition; + + } + + /** + * An expression. + */ + abstract static class Expression { + + static final int PRECEDENCE_OR = 1, PRECEDENCE_AND = 2, + PRECEDENCE_CONDITION = 3, PRECEDENCE_OPERAND = 4; + + /** + * Whether this is a condition. + * + * @return true if it is + */ + boolean isCondition() { + return false; + } + + /** + * Get the operator / operation precedence. The JCR specification uses: + * 1=OR, 2=AND, 3=condition, 4=operand + * + * @return the precedence (as an example, multiplication needs to return + * a higher number than addition) + */ + int getPrecedence() { + return PRECEDENCE_OPERAND; + } + + /** + * Get the column alias name of an expression. For a property, this is the + * property name (no matter how many selectors the query contains); for + * other expressions it matches the toString() method. + * + * @return the simple column name + */ + String getColumnAliasName() { + return toString(); + } + + } + + /** + * A selector parameter. + */ + static class SelectorExpr extends Expression { + + private final Selector selector; + + SelectorExpr(Selector selector) { + this.selector = selector; + } + + @Override + public String toString() { + return selector.name; + } + + } + + /** + * A literal expression. + */ + static class Literal extends Expression { + + final String value; + + Literal(String value) { + this.value = value; + } + + public static Expression newBoolean(boolean value) { + return new Literal(String.valueOf(value)); + } + + static Literal newNumber(String s) { + return new Literal(s); + } + + static Literal newString(String s) { + return new Literal(SQL2Parser.escapeStringLiteral(s)); + } + + @Override + public String toString() { + return value; + } + + } + + /** + * A property expression. + */ + static class Property extends Expression { + + final Selector selector; + final String name; + + Property(Selector selector, String name) { + this.selector = selector; + this.name = name; + } + + @Override + public String toString() { + StringBuilder buff = new StringBuilder(); + if (!selector.onlySelector) { + buff.append(selector.name).append('.'); + } + if (name.equals("*")) { + buff.append('*'); + } else { + buff.append('[').append(name).append(']'); + } + return buff.toString(); + } + + @Override + public String getColumnAliasName() { + return name; + } + + } + + /** + * A condition. + */ + static class Condition extends Expression { + + final Expression left; + final String operator; + Expression right; + final int precedence; + + /** + * Create a new condition. + * + * @param left the left hand side operator, or null + * @param operator the operator + * @param right the right hand side operator, or null + * @param precedence the operator precedence (Expression.PRECEDENCE_...) + */ + Condition(Expression left, String operator, Expression right, int precedence) { + this.left = left; + this.operator = operator; + this.right = right; + this.precedence = precedence; + } + + @Override + int getPrecedence() { + return precedence; + } + + @Override + public String toString() { + StringBuilder buff = new StringBuilder(); + if (left != null) { + if (left.getPrecedence() < precedence) { + buff.append('(').append(left.toString()).append(')'); + } else { + buff.append(left.toString()); + } + buff.append(' '); + } + buff.append(operator); + if (right != null) { + buff.append(' '); + if (right.getPrecedence() < precedence) { + buff.append('(').append(right.toString()).append(')'); + } else { + buff.append(right.toString()); + } + } + return buff.toString(); + } + + @Override + boolean isCondition() { + return true; + } + + } + + /** + * A function call. + */ + static class Function extends Expression { + + final String name; + final ArrayList params = new ArrayList(); + + Function(String name) { + this.name = name; + } + + @Override + public String toString() { + StringBuilder buff = new StringBuilder(name); + buff.append('('); + for (int i = 0; i < params.size(); i++) { + if (i > 0) { + buff.append(", "); + } + buff.append(params.get(i).toString()); + } + buff.append(')'); + return buff.toString(); + } + + @Override + boolean isCondition() { + return name.equals("contains") || name.equals("not"); + } + + } + + /** + * A cast operation. + */ + static class Cast extends Expression { + + final Expression expr; + final String type; + + Cast(Expression expr, String type) { + this.expr = expr; + this.type = type; + } + + @Override + public String toString() { + StringBuilder buff = new StringBuilder("cast("); + buff.append(expr.toString()); + buff.append(" as ").append(type).append(')'); + return buff.toString(); + } + + @Override + boolean isCondition() { + return false; + } + + } + + /** + * An order by expression. + */ + static class Order { + + boolean descending; + Expression expr; + + @Override + public String toString() { + return expr + (descending ? " desc" : ""); + } + + } + +} + diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AndImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AndImpl.java new file mode 100644 index 00000000000..f740e01d48f --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AndImpl.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import org.apache.jackrabbit.oak.query.index.FilterImpl; + +/** + * An AND condition. + */ +public class AndImpl extends ConstraintImpl { + + private final ConstraintImpl constraint1, constraint2; + + public AndImpl(ConstraintImpl constraint1, ConstraintImpl constraint2) { + this.constraint1 = constraint1; + this.constraint2 = constraint2; + } + + public ConstraintImpl getConstraint1() { + return constraint1; + } + + public ConstraintImpl getConstraint2() { + return constraint2; + } + + @Override + public boolean evaluate() { + return constraint1.evaluate() && constraint2.evaluate(); + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return protect(constraint1) + " and " + protect(constraint2); + } + + @Override + public void restrict(FilterImpl f) { + constraint1.restrict(f); + constraint2.restrict(f); + } + + @Override + public void restrictPushDown(SelectorImpl s) { + constraint1.restrictPushDown(s); + constraint2.restrictPushDown(s); + } + +} + diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElement.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElement.java new file mode 100644 index 00000000000..48cbe0299fe --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElement.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.query.Query; + +/** + * The base class for all abstract syntax tree nodes. + */ +abstract class AstElement { + + protected Query query; + + abstract boolean accept(AstVisitor v); + + protected String protect(Object expression) { + String str = expression.toString(); + if (str.indexOf(' ') >= 0) { + return '(' + str + ')'; + } else { + return str; + } + } + + protected String quote(String pathOrName) { + return '[' + pathOrName + ']'; + } + + public void setQuery(Query query) { + this.query = query; + } + + /** + * Calculate the absolute path (the path including the workspace name). + * + * @param path the session local path + * @return the absolute path + */ + protected String getAbsolutePath(String path) { + return path; + } + + /** + * Calculate the session local path (the path excluding the workspace name) + * if possible. + * + * @param path the absolute path + * @return the session local path, or null if not within this workspace + */ + protected String getLocalPath(String path) { + return path; + } + + protected Tree getTree(String path) { + return query.getTree(path); + } + +} + diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java new file mode 100644 index 00000000000..cf854e8be4d --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law + * or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import org.apache.jackrabbit.oak.api.PropertyValue; + +/** + * A factory for syntax tree elements. + */ +public class AstElementFactory { + + public AndImpl and(ConstraintImpl constraint1, ConstraintImpl constraint2) { + return new AndImpl((ConstraintImpl) constraint1, (ConstraintImpl) constraint2); + } + + public OrderingImpl ascending(DynamicOperandImpl operand) { + return new OrderingImpl((DynamicOperandImpl) operand, Order.ASCENDING); + } + + public BindVariableValueImpl bindVariable(String bindVariableName) { + return new BindVariableValueImpl(bindVariableName); + } + + public ChildNodeImpl childNode(String selectorName, String path) { + return new ChildNodeImpl(selectorName, path); + } + + public ChildNodeJoinConditionImpl childNodeJoinCondition(String childSelectorName, String parentSelectorName) + { + return new ChildNodeJoinConditionImpl(childSelectorName, parentSelectorName); + } + + public ColumnImpl column(String selectorName, String propertyName, String columnName) { + return new ColumnImpl(selectorName, propertyName, columnName); + } + + public ComparisonImpl comparison(DynamicOperandImpl operand1, Operator operator, StaticOperandImpl operand2) { + return new ComparisonImpl((DynamicOperandImpl) operand1, operator, (StaticOperandImpl) operand2); + } + + public DescendantNodeImpl descendantNode(String selectorName, String path) { + return new DescendantNodeImpl(selectorName, path); + } + + public DescendantNodeJoinConditionImpl descendantNodeJoinCondition(String descendantSelectorName, + String ancestorSelectorName) { + return new DescendantNodeJoinConditionImpl(descendantSelectorName, ancestorSelectorName); + } + + public OrderingImpl descending(DynamicOperandImpl operand) { + return new OrderingImpl(operand, Order.DESCENDING); + } + + public EquiJoinConditionImpl equiJoinCondition(String selector1Name, String property1Name, String selector2Name, + String property2Name) { + return new EquiJoinConditionImpl(selector1Name, property1Name, selector2Name, property2Name); + } + + public FullTextSearchImpl fullTextSearch(String selectorName, String propertyName, + StaticOperandImpl fullTextSearchExpression) { + return new FullTextSearchImpl(selectorName, propertyName, fullTextSearchExpression); + } + + public FullTextSearchScoreImpl fullTextSearchScore(String selectorName) { + return new FullTextSearchScoreImpl(selectorName); + } + + public JoinImpl join(SourceImpl left, SourceImpl right, JoinType joinType, JoinConditionImpl joinCondition) { + return new JoinImpl(left, right, joinType, joinCondition); + } + + public LengthImpl length(PropertyValueImpl propertyValue) { + return new LengthImpl(propertyValue); + } + + public LiteralImpl literal(PropertyValue literalValue) { + return new LiteralImpl(literalValue); + } + + public LowerCaseImpl lowerCase(DynamicOperandImpl operand) { + return new LowerCaseImpl((DynamicOperandImpl) operand); + } + + public NodeLocalNameImpl nodeLocalName(String selectorName) { + return new NodeLocalNameImpl(selectorName); + } + + public NodeNameImpl nodeName(String selectorName) { + return new NodeNameImpl(selectorName); + } + + public NotImpl not(ConstraintImpl constraint) { + return new NotImpl((ConstraintImpl) constraint); + } + + public OrImpl or(ConstraintImpl constraint1, ConstraintImpl constraint2) { + return new OrImpl((ConstraintImpl) constraint1, (ConstraintImpl) constraint2); + } + + public PropertyExistenceImpl propertyExistence(String selectorName, String propertyName) { + return new PropertyExistenceImpl(selectorName, propertyName); + } + + public PropertyValueImpl propertyValue(String selectorName, String propertyName) { + return new PropertyValueImpl(selectorName, propertyName); + } + + public PropertyValueImpl propertyValue(String selectorName, String propertyName, String propertyType) { + return new PropertyValueImpl(selectorName, propertyName, propertyType); + } + + public SameNodeImpl sameNode(String selectorName, String path) { + return new SameNodeImpl(selectorName, path); + } + + public SameNodeJoinConditionImpl sameNodeJoinCondition(String selector1Name, String selector2Name, String selector2Path) { + return new SameNodeJoinConditionImpl(selector1Name, selector2Name, selector2Path); + } + + public SelectorImpl selector(String nodeTypeName, String selectorName) { + return new SelectorImpl(nodeTypeName, selectorName); + } + + public UpperCaseImpl upperCase(DynamicOperandImpl operand) { + return new UpperCaseImpl(operand); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitor.java new file mode 100644 index 00000000000..cbce7449e84 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitor.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +/** + * A visitor to access all elements. + */ +public interface AstVisitor { + + boolean visit(AndImpl node); + + boolean visit(BindVariableValueImpl node); + + boolean visit(ChildNodeImpl node); + + boolean visit(ChildNodeJoinConditionImpl node); + + boolean visit(ColumnImpl node); + + boolean visit(ComparisonImpl node); + + boolean visit(DescendantNodeImpl node); + + boolean visit(DescendantNodeJoinConditionImpl node); + + boolean visit(EquiJoinConditionImpl node); + + boolean visit(FullTextSearchImpl node); + + boolean visit(FullTextSearchScoreImpl node); + + boolean visit(JoinImpl node); + + boolean visit(LengthImpl node); + + boolean visit(LiteralImpl node); + + boolean visit(LowerCaseImpl node); + + boolean visit(NodeLocalNameImpl node); + + boolean visit(NodeNameImpl node); + + boolean visit(NotImpl node); + + boolean visit(OrderingImpl node); + + boolean visit(OrImpl node); + + boolean visit(PropertyExistenceImpl node); + + boolean visit(PropertyValueImpl node); + + boolean visit(SameNodeImpl node); + + boolean visit(SameNodeJoinConditionImpl node); + + boolean visit(SelectorImpl node); + + boolean visit(UpperCaseImpl node); + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitorBase.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitorBase.java new file mode 100644 index 00000000000..81344215ae7 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitorBase.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import org.apache.jackrabbit.oak.query.Query; + +/** + * The base class to visit all elements. + */ +public abstract class AstVisitorBase implements AstVisitor { + + /** + * Calls accept on each of the attached constraints of the AND node. + */ + @Override + public boolean visit(AndImpl node) { + node.getConstraint1().accept(this); + node.getConstraint2().accept(this); + return true; + } + + /** + * Calls accept on the two operands in the comparison node. + */ + @Override + public boolean visit(ComparisonImpl node) { + node.getOperand1().accept(this); + node.getOperand2().accept(this); + return true; + } + + /** + * Calls accept on the static operand in the fulltext search constraint. + */ + @Override + public boolean visit(FullTextSearchImpl node) { + node.getFullTextSearchExpression().accept(this); + return true; + } + + /** + * Calls accept on the two sources and the join condition in the join node. + */ + @Override + public boolean visit(JoinImpl node) { + node.getRight().accept(this); + node.getLeft().accept(this); + node.getJoinCondition().accept(this); + return true; + } + + /** + * Calls accept on the property value in the length node. + */ + @Override + public boolean visit(LengthImpl node) { + return node.getPropertyValue().accept(this); + } + + /** + * Calls accept on the dynamic operand in the lower-case node. + */ + @Override + public boolean visit(LowerCaseImpl node) { + return node.getOperand().accept(this); + } + + /** + * Calls accept on the constraint in the NOT node. + */ + @Override + public boolean visit(NotImpl node) { + return node.getConstraint().accept(this); + } + + /** + * Calls accept on the dynamic operand in the ordering node. + */ + @Override + public boolean visit(OrderingImpl node) { + return node.getOperand().accept(this); + } + + /** + * Calls accept on each of the attached constraints of the OR node. + */ + @Override + public boolean visit(OrImpl node) { + node.getConstraint1().accept(this); + node.getConstraint2().accept(this); + return true; + } + + /** + * Calls accept on the following contained QOM nodes: + *
    + *
  • Source
  • + *
  • Constraints
  • + *
  • Orderings
  • + *
  • Columns
  • + *
+ * + * @param query the query to visit + */ + public void visit(Query query) { + query.getSource().accept(this); + ConstraintImpl constraint = query.getConstraint(); + if (constraint != null) { + constraint.accept(this); + } + OrderingImpl[] orderings = query.getOrderings(); + if (orderings != null) { + for (OrderingImpl ordering : orderings) { + ordering.accept(this); + } + } + ColumnImpl[] columns = query.getColumns(); + for (ColumnImpl column : columns) { + column.accept(this); + } + } + + /** + * Calls accept on the dynamic operand in the lower-case node. + */ + @Override + public boolean visit(UpperCaseImpl node) { + return node.getOperand().accept(this); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/BindVariableValueImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/BindVariableValueImpl.java new file mode 100644 index 00000000000..c59cc0467e2 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/BindVariableValueImpl.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.query.ast; + +import org.apache.jackrabbit.oak.api.PropertyValue; + + +/** + * A bind variable. + */ +public class BindVariableValueImpl extends StaticOperandImpl { + + private final String bindVariableName; + + public BindVariableValueImpl(String bindVariableName) { + this.bindVariableName = bindVariableName; + } + + public String getBindVariableName() { + return bindVariableName; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return '$' + bindVariableName; + } + + @Override + PropertyValue currentValue() { + return query.getBindVariableValue(bindVariableName); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ChildNodeImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ChildNodeImpl.java new file mode 100644 index 00000000000..b40377c3848 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ChildNodeImpl.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.Filter; + +/** + * The "ischildnode(...)" condition. + */ +public class ChildNodeImpl extends ConstraintImpl { + + private final String selectorName; + private final String parentPath; + private SelectorImpl selector; + + public ChildNodeImpl(String selectorName, String parentPath) { + this.selectorName = selectorName; + this.parentPath = parentPath; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return "ischildnode(" + quote(selectorName) + ", " + quote(parentPath) + ')'; + } + + public void bindSelector(SourceImpl source) { + selector = source.getExistingSelector(selectorName); + } + + @Override + public boolean evaluate() { + String p = selector.currentPath(); + String local = getLocalPath(p); + if (local == null) { + // not a local path + return false; + } + // the parent of the root is the root, + // so we need to special case this + return !PathUtils.denotesRoot(local) && PathUtils.getParentPath(local).equals(parentPath); + } + + @Override + public void restrict(FilterImpl f) { + if (selector == f.getSelector()) { + String path = getAbsolutePath(parentPath); + f.restrictPath(path, Filter.PathRestriction.DIRECT_CHILDREN); + } + } + + @Override + public void restrictPushDown(SelectorImpl s) { + if (s == selector) { + s.restrictSelector(this); + } + } + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ChildNodeJoinConditionImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ChildNodeJoinConditionImpl.java new file mode 100644 index 00000000000..7de63a1523f --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ChildNodeJoinConditionImpl.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.Filter; + +/** + * The "ischildnode(...)" join condition. + */ +public class ChildNodeJoinConditionImpl extends JoinConditionImpl { + + private final String childSelectorName; + private final String parentSelectorName; + private SelectorImpl childSelector; + private SelectorImpl parentSelector; + + public ChildNodeJoinConditionImpl(String childSelectorName, String parentSelectorName) { + this.childSelectorName = childSelectorName; + this.parentSelectorName = parentSelectorName; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return "ischildnode(" + quote(childSelectorName) + + ", " + quote(parentSelectorName) + ')'; + } + + public void bindSelector(SourceImpl source) { + parentSelector = source.getExistingSelector(parentSelectorName); + childSelector = source.getExistingSelector(childSelectorName); + } + + @Override + public boolean evaluate() { + String p = parentSelector.currentPath(); + String c = childSelector.currentPath(); + // the parent of the root is the root, + // so we need to special case this + return !PathUtils.denotesRoot(c) && PathUtils.getParentPath(c).equals(p); + } + + @Override + public void restrict(FilterImpl f) { + String p = parentSelector.currentPath(); + String c = childSelector.currentPath(); + if (f.getSelector() == parentSelector && c != null) { + f.restrictPath(PathUtils.getParentPath(c), Filter.PathRestriction.EXACT); + } + if (f.getSelector() == childSelector && p != null) { + f.restrictPath(p, Filter.PathRestriction.DIRECT_CHILDREN); + } + } + + @Override + public void restrictPushDown(SelectorImpl s) { + // nothing to do + } + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ColumnImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ColumnImpl.java new file mode 100644 index 00000000000..e0e7282e8d7 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ColumnImpl.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; + +/** + * A result column expression. + */ +public class ColumnImpl extends AstElement { + + private final String selectorName, propertyName, columnName; + private SelectorImpl selector; + + public ColumnImpl(String selectorName, String propertyName, String columnName) { + this.selectorName = selectorName; + this.propertyName = propertyName; + this.columnName = columnName; + } + + public String getColumnName() { + return columnName; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + if (propertyName != null) { + return quote(selectorName) + '.' + quote(propertyName) + + " as " + quote(columnName); + } else { + return quote(selectorName) + ".*"; + } + } + + public PropertyValue currentProperty() { + if (propertyName == null) { + // TODO for SELECT * FROM queries, currently return the path (for testing only) + String p = selector.currentPath(); + if (p == null) { + return null; + } + return PropertyValues.newString(p); + } + return selector.currentProperty(propertyName); + } + + public void bindSelector(SourceImpl source) { + selector = source.getExistingSelector(selectorName); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ComparisonImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ComparisonImpl.java new file mode 100644 index 00000000000..0efbae8bf1a --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ComparisonImpl.java @@ -0,0 +1,347 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; + +/** + * A comparison operation (including "like"). + */ +public class ComparisonImpl extends ConstraintImpl { + + private final DynamicOperandImpl operand1; + private final Operator operator; + private final StaticOperandImpl operand2; + + public ComparisonImpl(DynamicOperandImpl operand1, Operator operator, StaticOperandImpl operand2) { + this.operand1 = operand1; + this.operator = operator; + this.operand2 = operand2; + } + + public DynamicOperandImpl getOperand1() { + return operand1; + } + + public String getOperator() { + return operator.toString(); + } + + public StaticOperandImpl getOperand2() { + return operand2; + } + + public static int getType(PropertyValue p, int ifUnknown) { + if (p.count() > 0) { + return p.getType().tag(); + } + return ifUnknown; + } + + @Override + public boolean evaluate() { + // JCR 2.0 spec, 6.7.16 Comparison: + // "operand1 may evaluate to an array of values" + PropertyValue p1 = operand1.currentProperty(); + if (p1 == null) { + return false; + } + PropertyValue p2 = operand2.currentValue(); + if (p2 == null) { + // if the property doesn't exist, the result is always false + // even for "null <> 'x'" (same as in SQL) + return false; + } + int v1Type = getType(p1, p2.getType().tag()); + if (v1Type != p2.getType().tag()) { + // "the value of operand2 is converted to the + // property type of the value of operand1" + p2 = PropertyValues.convert(p2, v1Type, query.getNamePathMapper()); + if (p2 == null) { + return false; + } + } + return evaluate(p1, p2); + } + + /** + * "operand2 always evaluates to a scalar value" + * + * for multi-valued properties: if any of the value matches, then return true + * + * @param p1 + * @param p2 + * @return + */ + private boolean evaluate(PropertyValue p1, PropertyValue p2) { + switch (operator) { + case EQUAL: + return PropertyValues.match(p1, p2); + case NOT_EQUAL: + return !PropertyValues.match(p1, p2); + case GREATER_OR_EQUAL: + return p1.compareTo(p2) >= 0; + case GREATER_THAN: + return p1.compareTo(p2) > 0; + case LESS_OR_EQUAL: + return p1.compareTo(p2) <= 0; + case LESS_THAN: + return p1.compareTo(p2) < 0; + case LIKE: + return evaluateLike(p1, p2); + } + throw new IllegalArgumentException("Unknown operator: " + operator); + } + + private static boolean evaluateLike(PropertyValue v1, PropertyValue v2) { + LikePattern like = new LikePattern(v2.getValue(Type.STRING)); + for (String s : v1.getValue(Type.STRINGS)) { + if (like.matches(s)) { + return true; + } + } + return false; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return operand1 + " " + operator + " " + operand2; + } + + @Override + public void restrict(FilterImpl f) { + PropertyValue v = operand2.currentValue(); + if (v != null) { + // operand1.restrict(f, operator, v); + // TODO OAK-347 + if (operator == Operator.LIKE) { + String pattern; + pattern = v.getValue(Type.STRING); + LikePattern p = new LikePattern(pattern); + String lowerBound = p.getLowerBound(); + String upperBound = p.getUpperBound(); + if (lowerBound == null && upperBound == null) { + // ignore + } else if (lowerBound.equals(upperBound)) { + // no wildcards + operand1.restrict(f, Operator.EQUAL, v); + } else if (operand1.supportsRangeConditions()) { + if (lowerBound != null) { + PropertyValue pv = PropertyValues.newString(lowerBound); + operand1.restrict(f, Operator.GREATER_OR_EQUAL, pv); + } + if (upperBound != null) { + PropertyValue pv = PropertyValues.newString(upperBound); + operand1.restrict(f, Operator.LESS_OR_EQUAL, pv); + } + } else { + // path conditions + operand1.restrict(f, operator, v); + } + } else { + operand1.restrict(f, operator, v); + } + } + } + + @Override + public void restrictPushDown(SelectorImpl s) { + if (operand2.currentValue() != null) { + if (operand1.canRestrictSelector(s)) { + s.restrictSelector(this); + } + } + } + + /** + * A pattern matcher. + */ + public static class LikePattern { + + // TODO LIKE: optimize condition to '=' when no patterns are used, or 'between x and x+1' + // TODO LIKE: what to do for invalid patterns (patterns ending with a backslash) + + private static final int MATCH = 0, ONE = 1, ANY = 2; + + private String patternString; + private boolean invalidPattern; + private char[] patternChars; + private int[] patternTypes; + private int patternLength; + private String lowerBounds, upperBound; + + public LikePattern(String pattern) { + initPattern(pattern); + initBounds(); + } + + public boolean matches(String value) { + return !invalidPattern && compareAt(value, 0, 0, value.length(), patternChars, patternTypes); + } + + private static boolean compare(char[] pattern, String s, int pi, int si) { + return pattern[pi] == s.charAt(si); + } + + private boolean compareAt(String s, int pi, int si, int sLen, char[] pattern, int[] types) { + for (; pi < patternLength; pi++) { + int type = types[pi]; + switch (type) { + case MATCH: + if (si >= sLen || !compare(pattern, s, pi, si++)) { + return false; + } + break; + case ONE: + if (si++ >= sLen) { + return false; + } + break; + case ANY: + if (++pi >= patternLength) { + return true; + } + while (si < sLen) { + if (compare(pattern, s, pi, si) && compareAt(s, pi, si, sLen, pattern, types)) { + return true; + } + si++; + } + return false; + default: + throw new IllegalArgumentException("Internal error: " + type); + } + } + return si == sLen; + } + + private void initPattern(String p) { + patternLength = 0; + if (p == null) { + patternTypes = null; + patternChars = null; + return; + } + int len = p.length(); + patternChars = new char[len]; + patternTypes = new int[len]; + boolean lastAny = false; + for (int i = 0; i < len; i++) { + char c = p.charAt(i); + int type; + if (c == '\\') { + if (i >= len - 1) { + invalidPattern = true; + return; + } + c = p.charAt(++i); + type = MATCH; + lastAny = false; + } else if (c == '%') { + if (lastAny) { + continue; + } + type = ANY; + lastAny = true; + } else if (c == '_') { + type = ONE; + } else { + type = MATCH; + lastAny = false; + } + patternTypes[patternLength] = type; + patternChars[patternLength++] = c; + } + for (int i = 0; i < patternLength - 1; i++) { + if (patternTypes[i] == ANY && patternTypes[i + 1] == ONE) { + patternTypes[i] = ONE; + patternTypes[i + 1] = ANY; + } + } + patternString = new String(patternChars, 0, patternLength); + } + + @Override + public String toString() { + return patternString; + } + + /** + * Get the lower bound if any. + * + * @return return the lower bound, or null if unbound + */ + public String getLowerBound() { + return lowerBounds; + } + + /** + * Get the upper bound if any. + * + * @return return the upper bound, or null if unbound + */ + public String getUpperBound() { + return upperBound; + } + + private void initBounds() { + if (invalidPattern) { + return; + } + if (patternLength <= 0 || patternTypes[0] != MATCH) { + // can't use an index + return; + } + int maxMatch = 0; + StringBuilder buff = new StringBuilder(); + while (maxMatch < patternLength && patternTypes[maxMatch] == MATCH) { + buff.append(patternChars[maxMatch++]); + } + String lower = buff.toString(); + if (lower.isEmpty()) { + return; + } + if (maxMatch == patternLength) { + lowerBounds = upperBound = lower; + return; + } + lowerBounds = lower; + char next = lower.charAt(lower.length() - 1); + // search the 'next' unicode character (or at least a character + // that is higher) + for (int i = 1; i < 2000; i++) { + String upper = lower.substring(0, lower.length() - 1) + (char) (next + i); + if (upper.compareTo(lower) > 0) { + upperBound = upper; + return; + } + } + } + + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ConstraintImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ConstraintImpl.java new file mode 100644 index 00000000000..073eac6ea28 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ConstraintImpl.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import org.apache.jackrabbit.oak.query.index.FilterImpl; + +/** + * The base class for constraints. + */ +public abstract class ConstraintImpl extends AstElement { + + /** + * Evaluate the result using the currently set values. + * + * @return true if the constraint matches + */ + public abstract boolean evaluate(); + + /** + * Apply the condition to the filter, further restricting the filter if + * possible. This may also verify the data types are compatible, and that + * paths are valid. + * + * @param f the filter + */ + public abstract void restrict(FilterImpl f); + + /** + * Push as much of the condition down to this selector, further restricting + * the selector condition if possible. + * + * @param s the selector + */ + public abstract void restrictPushDown(SelectorImpl s); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/DescendantNodeImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/DescendantNodeImpl.java new file mode 100644 index 00000000000..a3a1b94f019 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/DescendantNodeImpl.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.query.ast; + +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.Filter; + +/** + * The "isdescendantnode(...)" condition. + */ +public class DescendantNodeImpl extends ConstraintImpl { + + private final String selectorName; + private final String ancestorPath; + private SelectorImpl selector; + + public DescendantNodeImpl(String selectorName, String ancestorPath) { + this.selectorName = selectorName; + this.ancestorPath = ancestorPath; + } + + @Override + public boolean evaluate() { + String p = selector.currentPath(); + String path = getAbsolutePath(ancestorPath); + if (p == null || path == null) { + return false; + } + return PathUtils.isAncestor(path, p); + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return "isdescendantnode(" + quote(selectorName) + + ", " + quote(ancestorPath) + ')'; + } + + public void bindSelector(SourceImpl source) { + selector = source.getExistingSelector(selectorName); + } + + @Override + public void restrict(FilterImpl f) { + if (f.getSelector() == selector) { + String path = getAbsolutePath(ancestorPath); + f.restrictPath(path, Filter.PathRestriction.ALL_CHILDREN); + } + } + + @Override + public void restrictPushDown(SelectorImpl s) { + if (s == selector) { + s.restrictSelector(this); + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/DescendantNodeJoinConditionImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/DescendantNodeJoinConditionImpl.java new file mode 100644 index 00000000000..78380ba9ab1 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/DescendantNodeJoinConditionImpl.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.Filter; + +/** + * The "isdescendantnode(...)" join condition. + */ +public class DescendantNodeJoinConditionImpl extends JoinConditionImpl { + + private final String descendantSelectorName; + private final String ancestorSelectorName; + private SelectorImpl descendantSelector; + private SelectorImpl ancestorSelector; + + public DescendantNodeJoinConditionImpl(String descendantSelectorName, + String ancestorSelectorName) { + this.descendantSelectorName = descendantSelectorName; + this.ancestorSelectorName = ancestorSelectorName; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return "isdescendantnode(" + + quote(descendantSelectorName) + + ", " + quote(ancestorSelectorName) + ')'; + } + + public void bindSelector(SourceImpl source) { + descendantSelector = source.getExistingSelector(descendantSelectorName); + ancestorSelector = source.getExistingSelector(ancestorSelectorName); + } + + @Override + public boolean evaluate() { + String a = ancestorSelector.currentPath(); + String d = descendantSelector.currentPath(); + return PathUtils.isAncestor(a, d); + } + + @Override + public void restrict(FilterImpl f) { + String d = descendantSelector.currentPath(); + String a = ancestorSelector.currentPath(); + if (d != null && f.getSelector() == ancestorSelector) { + f.restrictPath(PathUtils.getParentPath(d), Filter.PathRestriction.PARENT); + } + if (a != null && f.getSelector() == descendantSelector) { + f.restrictPath(a, Filter.PathRestriction.DIRECT_CHILDREN); + } + } + + @Override + public void restrictPushDown(SelectorImpl s) { + // nothing to do + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/DynamicOperandImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/DynamicOperandImpl.java new file mode 100644 index 00000000000..268637d3e68 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/DynamicOperandImpl.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.query.ast; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.query.index.FilterImpl; + +/** + * The base class for dynamic operands (such as a function or property). + */ +public abstract class DynamicOperandImpl extends AstElement { + + public abstract PropertyValue currentProperty(); + + public abstract void restrict(FilterImpl f, Operator operator, PropertyValue v); + + /** + * Check whether the condition can be applied to a selector (to restrict the + * selector). The method may return true if the operand can be evaluated + * when the given selector and all previous selectors in the join can be + * evaluated. + * + * @param s the selector + * @return true if the condition can be applied + */ + public abstract boolean canRestrictSelector(SelectorImpl s); + + public boolean supportsRangeConditions() { + return true; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/EquiJoinConditionImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/EquiJoinConditionImpl.java new file mode 100644 index 00000000000..0fbd39df07a --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/EquiJoinConditionImpl.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; + +/** + * The "a.x = b.y" join condition. + */ +public class EquiJoinConditionImpl extends JoinConditionImpl { + + private final String property1Name; + private final String property2Name; + private final String selector1Name; + private final String selector2Name; + private SelectorImpl selector1; + private SelectorImpl selector2; + + public EquiJoinConditionImpl(String selector1Name, String property1Name, String selector2Name, + String property2Name) { + this.selector1Name = selector1Name; + this.property1Name = property1Name; + this.selector2Name = selector2Name; + this.property2Name = property2Name; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return quote(selector1Name) + '.' + quote(property1Name) + + " = " + quote(selector2Name) + '.' + quote(property2Name); + } + + public void bindSelector(SourceImpl source) { + selector1 = source.getExistingSelector(selector1Name); + selector2 = source.getExistingSelector(selector2Name); + } + + @Override + public boolean evaluate() { + PropertyValue p1 = selector1.currentProperty(property1Name); + if (p1 == null) { + return false; + } + PropertyValue p2 = selector2.currentProperty(property2Name); + if (p2 == null) { + return false; + } + if (!p1.isArray() && !p2.isArray()) { + // both are single valued + return PropertyValues.match(p1, p2); + } + // TODO what is the expected result of an equi join for multi-valued properties? + if (!p1.isArray() && p2.isArray()) { + if (p1.getType().tag() != p2.getType().tag()) { + p1 = PropertyValues.convert(p1, p2.getType().tag(), query.getNamePathMapper()); + } + if (p1 != null && PropertyValues.match(p1, p2)) { + return true; + } + return false; + } else if (p1.isArray() && !p2.isArray()) { + if (p1.getType().tag() != p2.getType().tag()) { + p2 = PropertyValues.convert(p2, p1.getType().tag(), query.getNamePathMapper()); + } + if (p2 != null && PropertyValues.match(p1, p2)) { + return true; + } + return false; + } + return PropertyValues.match(p1, p2); + } + + @Override + public void restrict(FilterImpl f) { + PropertyValue p1 = selector1.currentProperty(property1Name); + PropertyValue p2 = selector2.currentProperty(property2Name); + if (f.getSelector() == selector1 && p2 != null) { + if (!p2.isArray()) { + // TODO support join on multi-valued properties + f.restrictProperty(property1Name, Operator.EQUAL, p2); + } + } + if (f.getSelector() == selector2 && p1 != null) { + if (!p1.isArray()) { + // TODO support join on multi-valued properties + f.restrictProperty(property2Name, Operator.EQUAL, p1); + } + } + } + + @Override + public void restrictPushDown(SelectorImpl s) { + // both properties may not be null + if (s == selector1) { + PropertyExistenceImpl ex = new PropertyExistenceImpl(s.getSelectorName(), property1Name); + ex.bindSelector(s); + s.restrictSelector(ex); + } else if (s == selector2) { + PropertyExistenceImpl ex = new PropertyExistenceImpl(s.getSelectorName(), property2Name); + ex.bindSelector(s); + s.restrictSelector(ex); + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java new file mode 100644 index 00000000000..cb1f426d5f8 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java @@ -0,0 +1,418 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import java.text.ParseException; +import java.util.ArrayList; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.query.ast.ComparisonImpl.LikePattern; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; + +import static org.apache.jackrabbit.oak.api.Type.STRING; +import static org.apache.jackrabbit.oak.api.Type.STRINGS; + +/** + * A fulltext "contains(...)" condition. + */ +public class FullTextSearchImpl extends ConstraintImpl { + + private final String selectorName; + private final String propertyName; + private final StaticOperandImpl fullTextSearchExpression; + private SelectorImpl selector; + + public FullTextSearchImpl(String selectorName, String propertyName, + StaticOperandImpl fullTextSearchExpression) { + this.selectorName = selectorName; + this.propertyName = propertyName; + this.fullTextSearchExpression = fullTextSearchExpression; + } + + public StaticOperandImpl getFullTextSearchExpression() { + return fullTextSearchExpression; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("contains("); + builder.append(quote(selectorName)); + if (propertyName != null) { + builder.append('.'); + builder.append(quote(propertyName)); + builder.append(", "); + } else { + builder.append(".*, "); + } + builder.append(getFullTextSearchExpression()); + builder.append(')'); + return builder.toString(); + } + + @Override + public boolean evaluate() { + StringBuilder buff = new StringBuilder(); + if (propertyName != null) { + PropertyValue p = selector.currentProperty(propertyName); + if (p == null) { + return false; + } + appendString(buff, p); + } else { + Tree tree = getTree(selector.currentPath()); + if (tree == null) { + return false; + } + for (PropertyState p : tree.getProperties()) { + appendString(buff, PropertyValues.create(p)); + } + } + // TODO fulltext conditions: need a way to disable evaluation + // if a fulltext index is used, to avoid filtering too much + // (we don't know what exact options are used in the fulltext index) + // (stop word, special characters,...) + PropertyValue v = fullTextSearchExpression.currentValue(); + try { + FullTextExpression expr = FullTextParser.parse(v.getValue(Type.STRING)); + return expr.evaluate(buff.toString()); + } catch (ParseException e) { + throw new IllegalArgumentException("Invalid expression: " + fullTextSearchExpression, e); + } + } + + private static void appendString(StringBuilder buff, PropertyValue p) { + if (p.isArray()) { + for (String v : p.getValue(STRINGS)) { + buff.append(v).append(' '); + } + } else { + buff.append(p.getValue(STRING)).append(' '); + } + } + + public void bindSelector(SourceImpl source) { + selector = source.getExistingSelector(selectorName); + } + + @Override + public void restrict(FilterImpl f) { + if (propertyName != null) { + if (f.getSelector() == selector) { + f.restrictProperty(propertyName, Operator.NOT_EQUAL, null); + } + } + f.restrictFulltextCondition(fullTextSearchExpression.currentValue().getValue(Type.STRING)); + } + + @Override + public void restrictPushDown(SelectorImpl s) { + if (s == selector) { + selector.restrictSelector(this); + } + } + + /** + * A parser for fulltext condition literals. The grammar is defined in the + * + * JCR 2.0 specification, 6.7.19 FullTextSearch, + * as follows (a bit simplified): + *
+     * FullTextSearchLiteral ::= Disjunct {' OR ' Disjunct}
+     * Disjunct ::= Term {' ' Term}
+     * Term ::= ['-'] SimpleTerm
+     * SimpleTerm ::= Word | '"' Word {' ' Word} '"'
+     * 
+ */ + public static class FullTextParser { + + String text; + int parseIndex; + + public static FullTextExpression parse(String text) throws ParseException { + FullTextParser p = new FullTextParser(); + p.text = text; + FullTextExpression e = p.parseOr(); + return e; + } + + FullTextExpression parseOr() throws ParseException { + FullTextOr or = new FullTextOr(); + or.list.add(parseAnd()); + while (parseIndex < text.length()) { + if (text.substring(parseIndex).startsWith("OR ")) { + parseIndex += 3; + or.list.add(parseAnd()); + } else { + break; + } + } + return or.simplify(); + } + + FullTextExpression parseAnd() throws ParseException { + FullTextAnd and = new FullTextAnd(); + and.list.add(parseTerm()); + while (parseIndex < text.length()) { + if (text.substring(parseIndex).startsWith("OR ")) { + break; + } + and.list.add(parseTerm()); + } + return and.simplify(); + } + + FullTextExpression parseTerm() throws ParseException { + if (parseIndex >= text.length()) { + throw getSyntaxError("term"); + } + boolean not = false; + StringBuilder buff = new StringBuilder(); + char c = text.charAt(parseIndex); + if (c == '-') { + if (++parseIndex >= text.length()) { + throw getSyntaxError("term"); + } + not = true; + } + boolean escaped = false; + if (c == '\"') { + parseIndex++; + while (true) { + if (parseIndex >= text.length()) { + throw getSyntaxError("double quote"); + } + c = text.charAt(parseIndex++); + if (c == '\\') { + escaped = true; + if (parseIndex >= text.length()) { + throw getSyntaxError("escaped char"); + } + c = text.charAt(parseIndex++); + buff.append(c); + } else if (c == '\"') { + if (parseIndex < text.length() && text.charAt(parseIndex) != ' ') { + throw getSyntaxError("space"); + } + parseIndex++; + break; + } else { + buff.append(c); + } + } + } else { + do { + c = text.charAt(parseIndex++); + if (c == '\\') { + escaped = true; + if (parseIndex >= text.length()) { + throw getSyntaxError("escaped char"); + } + c = text.charAt(parseIndex++); + buff.append(c); + } else if (c == ' ') { + break; + } else { + buff.append(c); + } + } while (parseIndex < text.length()); + } + if (buff.length() == 0) { + throw getSyntaxError("term"); + } + String text = buff.toString(); + FullTextTerm term = new FullTextTerm(text, not, escaped); + return term.simplify(); + } + + private ParseException getSyntaxError(String expected) { + int index = Math.max(0, Math.min(parseIndex, text.length() - 1)); + String query = text.substring(0, index) + "(*)" + text.substring(index).trim(); + if (expected != null) { + query += "; expected: " + expected; + } + return new ParseException("FullText expression: " + query, index); + } + + } + + /** + * The base class for fulltext condition expression. + */ + public abstract static class FullTextExpression { + public abstract boolean evaluate(String value); + abstract FullTextExpression simplify(); + } + + /** + * A fulltext "and" condition. + */ + static class FullTextAnd extends FullTextExpression { + ArrayList list = new ArrayList(); + + @Override + public boolean evaluate(String value) { + for (FullTextExpression e : list) { + if (!e.evaluate(value)) { + return false; + } + } + return true; + } + + @Override + FullTextExpression simplify() { + return list.size() == 1 ? list.get(0) : this; + } + + @Override + public String toString() { + StringBuilder buff = new StringBuilder(); + int i = 0; + for (FullTextExpression e : list) { + if (i++ > 0) { + buff.append(' '); + } + buff.append(e.toString()); + } + return buff.toString(); + } + + } + + /** + * A fulltext "or" condition. + */ + static class FullTextOr extends FullTextExpression { + ArrayList list = new ArrayList(); + + @Override + public boolean evaluate(String value) { + for (FullTextExpression e : list) { + if (e.evaluate(value)) { + return true; + } + } + return false; + } + + @Override + FullTextExpression simplify() { + return list.size() == 1 ? list.get(0).simplify() : this; + } + + @Override + public String toString() { + StringBuilder buff = new StringBuilder(); + int i = 0; + for (FullTextExpression e : list) { + if (i++ > 0) { + buff.append(" OR "); + } + buff.append(e.toString()); + } + return buff.toString(); + } + + } + + /** + * A fulltext term, or a "not" term. + */ + static class FullTextTerm extends FullTextExpression { + private final boolean not; + private final String text; + private final String filteredText; + private final LikePattern like; + + FullTextTerm(String text, boolean not, boolean escaped) { + this.text = text; + this.not = not; + // for testFulltextIntercapSQL + // filter special characters such as ' + // to make tests pass, for example the + // FulltextQueryTest.testFulltextExcludeSQL, + // which searches for: + // "text ''fox jumps'' -other" + // (please note the two single quotes instead of + // double quotes before for and after jumps) + boolean pattern = false; + if (escaped) { + filteredText = text; + } else { + StringBuilder buff = new StringBuilder(); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '*') { + buff.append('%'); + pattern = true; + } else if (c == '?') { + buff.append('_'); + pattern = true; + } else if (c == '_') { + buff.append("\\_"); + pattern = true; + } else if (Character.isLetterOrDigit(c) || " +-:&".indexOf(c) >= 0) { + buff.append(c); + } + } + this.filteredText = buff.toString().toLowerCase(); + } + if (pattern) { + like = new LikePattern("%" + filteredText + "%"); + } else { + like = null; + } + } + + @Override + public boolean evaluate(String value) { + // for testFulltextIntercapSQL + value = value.toLowerCase(); + if (like != null) { + return like.matches(value); + } + if (not) { + return value.indexOf(filteredText) < 0; + } + return value.indexOf(filteredText) >= 0; + } + + @Override + FullTextExpression simplify() { + return this; + } + + @Override + public String toString() { + return (not ? "-" : "") + "\"" + text.replaceAll("\"", "\\\"") + "\""; + } + + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchScoreImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchScoreImpl.java new file mode 100644 index 00000000000..0aa5050350d --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchScoreImpl.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.query.Query; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; + +/** + * A fulltext search score expression. + */ +public class FullTextSearchScoreImpl extends DynamicOperandImpl { + + private final String selectorName; + private SelectorImpl selector; + + public FullTextSearchScoreImpl(String selectorName) { + this.selectorName = selectorName; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return "score(" + quote(selectorName) + ')'; + } + + @Override + public PropertyValue currentProperty() { + PropertyValue p = selector.currentProperty(Query.JCR_SCORE); + if (p == null) { + // TODO if score() is not supported by the index, use the value 0.0? + return PropertyValues.newDouble(0.0); + } + return p; + } + + public void bindSelector(SourceImpl source) { + selector = source.getExistingSelector(selectorName); + } + + @Override + public void restrict(FilterImpl f, Operator operator, PropertyValue v) { + if (f.getSelector() == selector) { + if (operator == Operator.NOT_EQUAL && v != null) { + // not supported + return; + } + f.restrictProperty(Query.JCR_SCORE, operator, v); + } + } + + @Override + public boolean canRestrictSelector(SelectorImpl s) { + return s == selector; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/JoinConditionImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/JoinConditionImpl.java new file mode 100644 index 00000000000..b3e2fd54d40 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/JoinConditionImpl.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.query.ast; + +import org.apache.jackrabbit.oak.query.index.FilterImpl; + +/** + * The base class for join conditions. + */ +public abstract class JoinConditionImpl extends AstElement { + + public abstract boolean evaluate(); + + public abstract void restrict(FilterImpl f); + + public abstract void restrictPushDown(SelectorImpl selectorImpl); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/JoinImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/JoinImpl.java new file mode 100644 index 00000000000..e6fcc94aa48 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/JoinImpl.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law + * or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import org.apache.jackrabbit.oak.query.Query; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * A join. + */ +public class JoinImpl extends SourceImpl { + + private final JoinConditionImpl joinCondition; + private JoinType joinType; + private SourceImpl left; + private SourceImpl right; + + private boolean leftNeedExecute, rightNeedExecute; + private boolean leftNeedNext; + private boolean foundJoinedRow; + private boolean end; + private NodeState root; + + public JoinImpl(SourceImpl left, SourceImpl right, JoinType joinType, + JoinConditionImpl joinCondition) { + this.left = left; + this.right = right; + this.joinType = joinType; + this.joinCondition = joinCondition; + } + + public JoinConditionImpl getJoinCondition() { + return joinCondition; + } + + public SourceImpl getLeft() { + return left; + } + + public SourceImpl getRight() { + return right; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String getPlan(NodeState root) { + return left.getPlan(root) + ' ' + joinType + + " " + right.getPlan(root) + " on " + joinCondition; + } + + @Override + public String toString() { + return left + " " + joinType + + " " + right + " on " + joinCondition; + } + + @Override + public void init(Query query) { + switch (joinType) { + case INNER: + left.addJoinCondition(joinCondition, false); + right.addJoinCondition(joinCondition, true); + break; + case LEFT_OUTER: + right.setOuterJoin(true); + left.addJoinCondition(joinCondition, false); + right.addJoinCondition(joinCondition, true); + break; + case RIGHT_OUTER: + // swap left and right + // TODO right outer join: verify whether converting + // to left outer join is always correct (given the current restrictions) + joinType = JoinType.LEFT_OUTER; + SourceImpl temp = left; + left = right; + right = temp; + right.setOuterJoin(true); + left.addJoinCondition(joinCondition, false); + right.addJoinCondition(joinCondition, true); + break; + } + left.setQueryConstraint(queryConstraint); + right.setQueryConstraint(queryConstraint); + right.init(query); + left.init(query); + } + + @Override + public void prepare() { + left.prepare(); + right.prepare(); + } + + @Override + public SelectorImpl getSelector(String selectorName) { + SelectorImpl s = left.getSelector(selectorName); + if (s == null) { + s = right.getSelector(selectorName); + } + return s; + } + + @Override + public void execute(NodeState root) { + this.root = root; + leftNeedExecute = true; + end = false; + } + + @Override + public boolean next() { + if (end) { + return false; + } + if (leftNeedExecute) { + left.execute(root); + leftNeedExecute = false; + leftNeedNext = true; + } + while (true) { + if (leftNeedNext) { + if (!left.next()) { + end = true; + return false; + } + leftNeedNext = false; + rightNeedExecute = true; + } + if (rightNeedExecute) { + right.execute(root); + foundJoinedRow = false; + rightNeedExecute = false; + } + if (!right.next()) { + leftNeedNext = true; + } else { + if (joinCondition.evaluate()) { + foundJoinedRow = true; + return true; + } + } + // for an outer join, if no matching result was found, + // one row returned (with all values set to null) + if (right.outerJoin && leftNeedNext && !foundJoinedRow) { + return true; + } + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/JoinType.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/JoinType.java new file mode 100644 index 00000000000..1c592d919ed --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/JoinType.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +/** + * Enumeration of the join types. + */ +public enum JoinType { + + INNER("inner join"), + + LEFT_OUTER("left outer join"), + + RIGHT_OUTER("right outer join"); + + /** + * The name of this join type. + */ + private final String name; + + JoinType(String name) { + this.name = name; + } + + /** + * Returns the join type. + */ + @Override + public String toString() { + return name; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/LengthImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/LengthImpl.java new file mode 100644 index 00000000000..477ea27ccea --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/LengthImpl.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import javax.jcr.PropertyType; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; + +/** + * The function "length(..)". + */ +public class LengthImpl extends DynamicOperandImpl { + + private final PropertyValueImpl propertyValue; + + public LengthImpl(PropertyValueImpl propertyValue) { + this.propertyValue = propertyValue; + } + + public PropertyValueImpl getPropertyValue() { + return propertyValue; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return "length(" + propertyValue + ')'; + } + + @Override + public PropertyValue currentProperty() { + PropertyValue p = propertyValue.currentProperty(); + if (p == null) { + return null; + } + if (!p.isArray()) { + long length = p.size(); + return PropertyValues.newLong(length); + } + // TODO what is the expected result for LENGTH(multiValueProperty)? + throw new IllegalArgumentException("LENGTH(x) on multi-valued property is not supported"); + } + + @Override + public void restrict(FilterImpl f, Operator operator, PropertyValue v) { + if (v != null) { + switch (v.getType().tag()) { + case PropertyType.LONG: + case PropertyType.DECIMAL: + case PropertyType.DOUBLE: + // ok - comparison with a number + break; + case PropertyType.BINARY: + case PropertyType.STRING: + case PropertyType.DATE: + // ok - compare with a string literal + break; + default: + throw new IllegalArgumentException( + "Can not compare the length with a constant of type " + + PropertyType.nameFromValue(v.getType().tag()) + + " and value " + v.toString()); + } + } + // LENGTH(x) implies x is not null + propertyValue.restrict(f, Operator.NOT_EQUAL, null); + } + + @Override + public boolean canRestrictSelector(SelectorImpl s) { + return propertyValue.canRestrictSelector(s); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/LiteralImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/LiteralImpl.java new file mode 100644 index 00000000000..7501208ae49 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/LiteralImpl.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import java.util.Locale; + +import javax.jcr.PropertyType; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.query.SQL2Parser; + +/** + * A literal of a certain data type, possibly "cast(..)" of a literal. + */ +public class LiteralImpl extends StaticOperandImpl { + + private final PropertyValue value; + + public LiteralImpl(PropertyValue value) { + this.value = value; + } + + public PropertyValue getLiteralValue() { + return value; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + String type = PropertyType.nameFromValue(value.getType().tag()); + return "cast(" + escape() + " as " + type.toLowerCase(Locale.ENGLISH) + ')'; + } + + private String escape() { + return SQL2Parser.escapeStringLiteral(value.getValue(Type.STRING)); + } + + @Override + PropertyValue currentValue() { + return value; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/LowerCaseImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/LowerCaseImpl.java new file mode 100644 index 00000000000..26ca88cb7a4 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/LowerCaseImpl.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; + +import static org.apache.jackrabbit.oak.api.Type.STRING; + +/** + * The function "lower(..)". + */ +public class LowerCaseImpl extends DynamicOperandImpl { + + private final DynamicOperandImpl operand; + + public LowerCaseImpl(DynamicOperandImpl operand) { + this.operand = operand; + } + + public DynamicOperandImpl getOperand() { + return operand; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return "lower(" + operand + ')'; + } + + @Override + public PropertyValue currentProperty() { + PropertyValue p = operand.currentProperty(); + if (p == null) { + return null; + } + // TODO what is the expected result of LOWER(x) for an array property? + // currently throws an exception + String value = p.getValue(STRING); + return PropertyValues.newString(value.toLowerCase()); + } + + @Override + public void restrict(FilterImpl f, Operator operator, PropertyValue v) { + // LOWER(x) implies x is not null + operand.restrict(f, Operator.NOT_EQUAL, null); + } + + @Override + public boolean canRestrictSelector(SelectorImpl s) { + return operand.canRestrictSelector(s); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NodeLocalNameImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NodeLocalNameImpl.java new file mode 100644 index 00000000000..7afebdf5623 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NodeLocalNameImpl.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.query.ast; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; +import org.apache.jackrabbit.util.ISO9075; + +/** + * The function "localname(..)". + */ +public class NodeLocalNameImpl extends DynamicOperandImpl { + + private final String selectorName; + private SelectorImpl selector; + + public NodeLocalNameImpl(String selectorName) { + this.selectorName = selectorName; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return "localname(" + quote(selectorName) + ')'; + } + + public void bindSelector(SourceImpl source) { + selector = source.getExistingSelector(selectorName); + } + + @Override + public PropertyValue currentProperty() { + String name = PathUtils.getName(selector.currentPath()); + // Name escaping (convert space to _x0020_) + name = ISO9075.encode(name); + int colon = name.indexOf(':'); + // TODO LOCALNAME: evaluation of local name might not be correct + String localName = colon < 0 ? name : name.substring(colon + 1); + return PropertyValues.newString(localName); + } + + @Override + public void restrict(FilterImpl f, Operator operator, PropertyValue v) { + // TODO support LOCALNAME index conditions + } + + @Override + public boolean canRestrictSelector(SelectorImpl s) { + return s == selector; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NodeNameImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NodeNameImpl.java new file mode 100644 index 00000000000..e9b1cb99e96 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NodeNameImpl.java @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import javax.jcr.PropertyType; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; +import org.apache.jackrabbit.util.ISO9075; + +/** + * The function "name(..)". + */ +public class NodeNameImpl extends DynamicOperandImpl { + + private final String selectorName; + private SelectorImpl selector; + + public NodeNameImpl(String selectorName) { + this.selectorName = selectorName; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return "name(" + quote(selectorName) + ')'; + } + + public void bindSelector(SourceImpl source) { + selector = source.getExistingSelector(selectorName); + } + + @Override + public boolean supportsRangeConditions() { + return false; + } + + @Override + public PropertyValue currentProperty() { + String path = selector.currentPath(); + // Name escaping (convert space to _x0020_) + String name = ISO9075.encode(PathUtils.getName(path)); + return PropertyValues.newName(name); + } + + @Override + public void restrict(FilterImpl f, Operator operator, PropertyValue v) { + if (v == null) { + return; + } + if (!isName(v)) { + throw new IllegalArgumentException("Invalid name value: " + v.toString()); + } + String path = v.getValue(Type.STRING); + // Name escaping (convert _x0020_ to space) + path = decodeName(path); + if (PathUtils.isAbsolute(path)) { + throw new IllegalArgumentException("NAME() comparison with absolute path are not allowed: " + path); + } + if (PathUtils.getDepth(path) > 1) { + throw new IllegalArgumentException("NAME() comparison with relative path are not allowed: " + path); + } + // TODO support NAME(..) index conditions + } + + @Override + public boolean canRestrictSelector(SelectorImpl s) { + return s == selector; + } + + private String decodeName(String path) { + // Name escaping (convert _x0020_ to space) + path = ISO9075.decode(path); + // normalize paths (./name > name) + path = PropertyValues.getOakPath(path, query.getNamePathMapper()); + return path; + } + + /** + * Validate that the given value can be converted to a JCR name. + * + * @param v the value + * @return true if it can be converted + */ + private static boolean isName(PropertyValue v) { + // TODO correctly validate JCR names - see JCR 2.0 spec 3.2.4 Naming Restrictions + switch (v.getType().tag()) { + case PropertyType.DATE: + case PropertyType.DECIMAL: + case PropertyType.DOUBLE: + case PropertyType.LONG: + case PropertyType.BOOLEAN: + return false; + } + String n = v.getValue(Type.STRING); + if (n.startsWith("[") && !n.endsWith("]")) { + return false; + } + return true; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NotImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NotImpl.java new file mode 100644 index 00000000000..f12901119c6 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NotImpl.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.query.ast; + +import org.apache.jackrabbit.oak.query.index.FilterImpl; + +/** + * A "not" condition. + */ +public class NotImpl extends ConstraintImpl { + + private final ConstraintImpl constraint; + + public NotImpl(ConstraintImpl constraint) { + this.constraint = constraint; + } + + public ConstraintImpl getConstraint() { + return constraint; + } + + @Override + public boolean evaluate() { + return !constraint.evaluate(); + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return "not " + protect(constraint); + } + + @Override + public void restrict(FilterImpl f) { + // ignore + // TODO convert NOT conditions + } + + @Override + public void restrictPushDown(SelectorImpl s) { + // ignore + // TODO convert NOT conditions + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/Operator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/Operator.java new file mode 100644 index 00000000000..0b474a87d5a --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/Operator.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.query.ast; + +/** + * Enumeration of operators. + */ +public enum Operator { + + EQUAL("="), + + NOT_EQUAL("<>"), + + GREATER_THAN(">"), + + GREATER_OR_EQUAL(">="), + + LESS_THAN("<"), + + LESS_OR_EQUAL("<="), + + LIKE("like"); + + /** + * The name of this operator. + */ + private final String name; + + Operator(String name) { + this.name = name; + } + + /** + * Returns the name of this query operator. + */ + @Override + public String toString() { + return name; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/OrImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/OrImpl.java new file mode 100644 index 00000000000..3ccbcd08845 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/OrImpl.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.query.ast; + +import org.apache.jackrabbit.oak.query.index.FilterImpl; + +/** + * An "or" condition. + */ +public class OrImpl extends ConstraintImpl { + + private final ConstraintImpl constraint1; + private final ConstraintImpl constraint2; + + public OrImpl(ConstraintImpl constraint1, ConstraintImpl constraint2) { + this.constraint1 = constraint1; + this.constraint2 = constraint2; + } + + public ConstraintImpl getConstraint1() { + return constraint1; + } + + public ConstraintImpl getConstraint2() { + return constraint2; + } + + @Override + public boolean evaluate() { + return constraint1.evaluate() || constraint2.evaluate(); + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return protect(constraint1) + " or " + protect(constraint2); + } + + @Override + public void restrict(FilterImpl f) { + // ignore + // TODO convert OR conditions to UNION + } + + @Override + public void restrictPushDown(SelectorImpl s) { + // ignore + // TODO some OR conditions can be applied to a selector, + // for example WHERE X.ID = 1 OR X.ID = 2 + // can be applied to X as a whole, + // but X.ID = 1 OR Y.ID = 2 can't be applied to either + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FilePathCache.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/Order.java similarity index 68% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FilePathCache.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/Order.java index 4a88c593ed9..17f149055a4 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/fs/FilePathCache.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/Order.java @@ -14,22 +14,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.fs; - -import java.io.IOException; -import java.nio.channels.FileChannel; +package org.apache.jackrabbit.oak.query.ast; /** - * A file system with a small read cache. + * Enumeration of query orders. */ -public class FilePathCache extends FilePathWrapper { +public enum Order { + + ASCENDING("asc"), + + DESCENDING("desc"); + + /** + * The name of this order. + */ + private final String name; - public FileChannel open(String mode) throws IOException { - return new FileCache(getBase().name, getBase().open(mode)); + Order(String name) { + this.name = name; } - public String getScheme() { - return "cache"; + /** + * @return the name of this order. + */ + public String getName() { + return name; } } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/OrderingImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/OrderingImpl.java new file mode 100644 index 00000000000..5b188f0f372 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/OrderingImpl.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.query.ast; + +/** + * An element of an "order by" list. This includes whether this element should + * be sorted in ascending or descending order. + */ +public class OrderingImpl extends AstElement { + + private final DynamicOperandImpl operand; + private final Order order; + + public OrderingImpl(DynamicOperandImpl operand, Order order) { + this.operand = operand; + this.order = order; + } + + public DynamicOperandImpl getOperand() { + return operand; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return operand + " " + order.name(); + } + + public boolean isDescending() { + return order == Order.DESCENDING; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PropertyExistenceImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PropertyExistenceImpl.java new file mode 100644 index 00000000000..f2261c9eccc --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PropertyExistenceImpl.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.query.ast; + +import org.apache.jackrabbit.oak.query.index.FilterImpl; + +/** + * A condition to check if the property exists ("is not null"). + */ +public class PropertyExistenceImpl extends ConstraintImpl { + + private final String selectorName; + private final String propertyName; + private SelectorImpl selector; + + public PropertyExistenceImpl(String selectorName, String propertyName) { + this.selectorName = selectorName; + this.propertyName = propertyName; + } + + @Override + public boolean evaluate() { + return selector.currentProperty(propertyName) != null; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return quote(selectorName) + '.' + quote(propertyName) + " is not null"; + } + + public void bindSelector(SourceImpl source) { + selector = source.getExistingSelector(selectorName); + } + + @Override + public void restrict(FilterImpl f) { + if (f.getSelector() == selector) { + f.restrictProperty(propertyName, Operator.NOT_EQUAL, null); + } + } + + @Override + public void restrictPushDown(SelectorImpl s) { + if (s == selector) { + s.restrictSelector(this); + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PropertyValueImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PropertyValueImpl.java new file mode 100644 index 00000000000..48783e0d91e --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PropertyValueImpl.java @@ -0,0 +1,186 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import javax.jcr.PropertyType; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.query.Query; +import org.apache.jackrabbit.oak.query.SQL2Parser; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; + +import com.google.common.collect.Iterables; + +/** + * A property expression. + */ +public class PropertyValueImpl extends DynamicOperandImpl { + + private final String selectorName; + private final String propertyName; + private final int propertyType; + private SelectorImpl selector; + + public PropertyValueImpl(String selectorName, String propertyName) { + this(selectorName, propertyName, null); + } + + public PropertyValueImpl(String selectorName, String propertyName, String propertyType) { + this.selectorName = selectorName; + this.propertyName = propertyName; + this.propertyType = propertyType == null ? + PropertyType.UNDEFINED : + SQL2Parser.getPropertyTypeFromName(propertyType); + } + + public String getSelectorName() { + return selectorName; + } + + public String getPropertyName() { + return propertyName; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + String s = quote(selectorName) + '.' + quote(propertyName); + if (propertyType != PropertyType.UNDEFINED) { + s = "property(" + s + ", '" + + PropertyType.nameFromValue(propertyType).toLowerCase(Locale.ENGLISH) + + "')"; + } + return s; + } + + @Override + public boolean supportsRangeConditions() { + // the jcr:path pseudo-property doesn't support LIKE conditions, + // because the path doesn't might be escaped, and possibly contain + // expressions that would result in incorrect results (/test[1] for example) + return !propertyName.equals(Query.JCR_PATH); + } + + @Override + public PropertyValue currentProperty() { + boolean relative = propertyName.indexOf('/') >= 0; + boolean asterisk = propertyName.equals("*"); + if (!relative && !asterisk) { + PropertyValue p = selector.currentProperty(propertyName); + return matchesPropertyType(p) ? p : null; + } + Tree tree = getTree(selector.currentPath()); + if (tree == null) { + return null; + } + if (relative) { + for (String p : PathUtils.elements(PathUtils.getParentPath(propertyName))) { + if (tree == null) { + return null; + } + if (!tree.hasChild(p)) { + return null; + } + tree = tree.getChild(p); + } + if (tree == null) { + return null; + } + } + if (!asterisk) { + String name = PathUtils.getName(propertyName); + if (!tree.hasProperty(name)) { + return null; + } + PropertyState p = tree.getProperty(name); + return matchesPropertyType(p) ? PropertyValues.create(p) : null; + } + // asterisk - create a multi-value property + // warning: the returned property state may have a mixed type + // (not all values may have the same type) + + // TODO currently all property values are converted to strings - + // this doesn't play well with the idea that the types may be different + List values = new ArrayList(); + for (PropertyState p : tree.getProperties()) { + if (matchesPropertyType(p)) { + Iterables.addAll(values, p.getValue(Type.STRINGS)); + } + } + // "*" + return PropertyValues.newString(values); + } + + private boolean matchesPropertyType(PropertyValue value) { + if (value == null) { + return false; + } + if (propertyType == PropertyType.UNDEFINED) { + return true; + } + return value.getType().tag() == propertyType; + } + + private boolean matchesPropertyType(PropertyState state) { + if (state == null) { + return false; + } + if (propertyType == PropertyType.UNDEFINED) { + return true; + } + return state.getType().tag() == propertyType; + } + + public void bindSelector(SourceImpl source) { + selector = source.getExistingSelector(selectorName); + } + + @Override + public void restrict(FilterImpl f, Operator operator, PropertyValue v) { + if (f.getSelector() == selector) { + if (operator == Operator.NOT_EQUAL && v != null) { + // not supported + return; + } + f.restrictProperty(propertyName, operator, v); + if (propertyType != PropertyType.UNDEFINED) { + f.restrictPropertyType(propertyName, operator, propertyType); + } + } + } + + @Override + public boolean canRestrictSelector(SelectorImpl s) { + return s == selector; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SameNodeImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SameNodeImpl.java new file mode 100644 index 00000000000..dcdf086f4df --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SameNodeImpl.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.Filter; + +/** + * The function "issamenode(..)". + */ +public class SameNodeImpl extends ConstraintImpl { + + private final String path; + private final String selectorName; + private SelectorImpl selector; + + public SameNodeImpl(String selectorName, String path) { + this.selectorName = selectorName; + this.path = path; + } + + @Override + public boolean evaluate() { + String p = getAbsolutePath(path); + // TODO normalize paths + return selector.currentPath().equals(p); + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return "issamenode(" + quote(selectorName) + + ", " + quote(path) + ')'; + } + + public void bindSelector(SourceImpl source) { + selector = source.getExistingSelector(selectorName); + } + + @Override + public void restrict(FilterImpl f) { + if (f.getSelector() == selector) { + String p = getAbsolutePath(path); + f.restrictPath(p, Filter.PathRestriction.EXACT); + } + // TODO validate absolute path + } + + @Override + public void restrictPushDown(SelectorImpl s) { + if (s == selector) { + s.restrictSelector(this); + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SameNodeJoinConditionImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SameNodeJoinConditionImpl.java new file mode 100644 index 00000000000..d2b3622aa39 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SameNodeJoinConditionImpl.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.Filter; + +/** + * The "issamenode(...)" join condition. + */ +public class SameNodeJoinConditionImpl extends JoinConditionImpl { + + private final String selector1Name; + private final String selector2Name; + private final String selector2Path; + private SelectorImpl selector1; + private SelectorImpl selector2; + + public SameNodeJoinConditionImpl(String selector1Name, String selector2Name, + String selector2Path) { + this.selector1Name = selector1Name; + this.selector2Name = selector2Name; + this.selector2Path = selector2Path; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("issamenode("); + builder.append(quote(selector1Name)); + builder.append(", "); + builder.append(quote(selector2Name)); + if (selector2Path != null) { + builder.append(", "); + builder.append(quote(selector2Path)); + } + builder.append(')'); + return builder.toString(); + } + + public void bindSelector(SourceImpl source) { + selector1 = source.getExistingSelector(selector1Name); + selector2 = source.getExistingSelector(selector2Name); + } + + @Override + public boolean evaluate() { + String p1 = selector1.currentPath(); + String p2 = selector2.currentPath(); + return p1.equals(p2); + } + + @Override + public void restrict(FilterImpl f) { + String p1 = selector1.currentPath(); + String p2 = selector2.currentPath(); + if (f.getSelector() == selector1) { + f.restrictPath(p2, Filter.PathRestriction.EXACT); + } + if (f.getSelector() == selector2) { + f.restrictPath(p1, Filter.PathRestriction.EXACT); + } + } + + @Override + public void restrictPushDown(SelectorImpl s) { + // nothing to do + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java new file mode 100644 index 00000000000..1ceee4e9294 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java @@ -0,0 +1,288 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import static org.apache.jackrabbit.oak.api.Type.STRINGS; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import javax.annotation.CheckForNull; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants; +import org.apache.jackrabbit.oak.plugins.nodetype.ReadOnlyNodeTypeManager; +import org.apache.jackrabbit.oak.query.Query; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.Cursor; +import org.apache.jackrabbit.oak.spi.query.Filter; +import org.apache.jackrabbit.oak.spi.query.IndexRow; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; +import org.apache.jackrabbit.oak.spi.query.QueryIndex; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import com.google.common.collect.ImmutableSet; + +/** + * A selector within a query. + */ +public class SelectorImpl extends SourceImpl { + + // TODO possibly support using multiple indexes (using index intersection / index merge) + protected QueryIndex index; + + private final String nodeTypeName, selectorName; + private Cursor cursor; + private int scanCount; + /** + * Iterable over selected node type and its subtypes + */ + private Iterable nodeTypes; + + /** + * The selector condition can be evaluated when the given selector is + * evaluated. For example, for the query + * "select * from nt:base a inner join nt:base b where a.x = 1 and b.y = 2", + * the condition "a.x = 1" can be evaluated when evaluating selector a. The + * other part of the condition can't be evaluated until b is available. + */ + private ConstraintImpl selectorCondition; + + public SelectorImpl(String nodeTypeName, String selectorName) { + this.nodeTypeName = nodeTypeName; + this.selectorName = selectorName; + } + + public String getSelectorName() { + return selectorName; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return quote(nodeTypeName) + " as " + quote(selectorName); + } + + + @Override + public void prepare() { + if (queryConstraint != null) { + queryConstraint.restrictPushDown(this); + } + for (JoinConditionImpl c : allJoinConditions) { + c.restrictPushDown(this); + } + index = query.getBestIndex(createFilter()); + } + + @Override + public void execute(NodeState root) { + cursor = index.query(createFilter(), root); + } + + @Override + public String getPlan(NodeState root) { + StringBuilder buff = new StringBuilder(); + buff.append(toString()); + buff.append(" /* ").append(index.getPlan(createFilter(), root)); + if (selectorCondition != null) { + buff.append(" where ").append(selectorCondition); + } + buff.append(" */"); + return buff.toString(); + } + + private Filter createFilter() { + FilterImpl f = new FilterImpl(this); + f.setNodeType(nodeTypeName); + if (joinCondition != null) { + joinCondition.restrict(f); + } + if (!outerJoin) { + // for outer joins, query constraints can't be applied to the + // filter, because that would alter the result + if (queryConstraint != null) { + queryConstraint.restrict(f); + } + } + return f; + } + + @Override + public boolean next() { + while (cursor != null && cursor.next()) { + scanCount++; + Tree tree = getTree(cursor.currentRow().getPath()); + if (tree == null) { + continue; + } + if (nodeTypeName != null + && !nodeTypeName.equals(JcrConstants.NT_BASE) + && !evaluateTypeMatch(tree)) { + continue; + } + if (selectorCondition != null && !selectorCondition.evaluate()) { + continue; + } + if (joinCondition != null && !joinCondition.evaluate()) { + continue; + } + return true; + } + return false; + } + + private boolean evaluateTypeMatch(Tree tree) { + Set primary = + getStrings(tree, JcrConstants.JCR_PRIMARYTYPE); + Set mixins = + getStrings(tree, JcrConstants.JCR_MIXINTYPES); + + try { + for (NodeType type : getNodeTypes()) { + if (evaluateTypeMatch(type, primary, mixins)) { + return true; + } + } + } catch (RepositoryException e) { + throw new RuntimeException( + "Unable to evaluate node type constraints", e); + } + + return false; + } + + private static Set getStrings(Tree tree, String name) { + ImmutableSet.Builder builder = ImmutableSet.builder(); + PropertyState property = tree.getProperty(name); + if (property != null) { + for (String value : property.getValue(STRINGS)) { + builder.add(value); + } + } + return builder.build(); + } + + private static boolean evaluateTypeMatch( + NodeType type, Set primary, Set mixins) { + String name = type.getName(); + if (type.isMixin()) { + return mixins.contains(name); + } else { + return primary.contains(name); + } + } + + /** + * Get the current absolute path (including workspace name) + * + * @return the path + */ + public String currentPath() { + return cursor == null ? null : cursor.currentRow().getPath(); + } + + public PropertyValue currentProperty(String propertyName) { + if (propertyName.equals(Query.JCR_PATH)) { + String p = currentPath(); + if (p == null) { + return null; + } + String local = getLocalPath(p); + if (local == null) { + // not a local path + return null; + } + return PropertyValues.newString(local); + } + if (cursor == null) { + return null; + } + IndexRow r = cursor.currentRow(); + if (r == null) { + return null; + } + // TODO support pseudo-properties such as jcr:score using + // r.getValue(columnName) + String path = r.getPath(); + if (path == null) { + return null; + } + Tree t = getTree(path); + return t == null ? null : PropertyValues.create(t.getProperty(propertyName)); + } + + @Override + public void init(Query query) { + // nothing to do + } + + @Override + public SelectorImpl getSelector(String selectorName) { + if (selectorName.equals(this.selectorName)) { + return this; + } + return null; + } + + public long getScanCount() { + return scanCount; + } + + public void restrictSelector(ConstraintImpl constraint) { + if (selectorCondition == null) { + selectorCondition = constraint; + } else { + selectorCondition = new AndImpl(selectorCondition, constraint); + } + } + + private Iterable getNodeTypes() throws RepositoryException { + if (nodeTypes == null) { + List types = new ArrayList(); + NodeTypeManager manager = new ReadOnlyNodeTypeManager() { + @Override @CheckForNull + protected Tree getTypes() { + return getTree(NodeTypeConstants.NODE_TYPES_PATH); + } + }; + NodeType type = manager.getNodeType(nodeTypeName); + types.add(type); + + NodeTypeIterator it = type.getSubtypes(); + while (it.hasNext()) { + types.add(it.nextNodeType()); + } + nodeTypes = types; + } + return nodeTypes; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SourceImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SourceImpl.java new file mode 100644 index 00000000000..6cad163b6f9 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SourceImpl.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import java.util.ArrayList; + +import org.apache.jackrabbit.oak.query.Query; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * The base class of a selector and a join. + */ +public abstract class SourceImpl extends AstElement { + + /** + * The WHERE clause of the query. + */ + protected ConstraintImpl queryConstraint; + + /** + * The join condition of this selector that can be evaluated at execution + * time. For the query "select * from nt:base as a inner join nt:base as b + * on a.x = b.x", the join condition "a.x = b.x" is only set for the + * selector b, as selector a can't evaluate it if it is executed first + * (until b is executed). + */ + protected JoinConditionImpl joinCondition; + + /** + * The list of all join conditions this selector is involved. For the query + * "select * from nt:base as a inner join nt:base as b on a.x = + * b.x", the join condition "a.x = b.x" is set for both selectors a and b, + * so both can check if the property x is set. + */ + protected ArrayList allJoinConditions = + new ArrayList(); + + /** + * Whether this selector is the right hand side of a join. + */ + protected boolean join; + + /** + * Whether this selector is the right hand side of a left outer join. + * Right outer joins are converted to left outer join. + */ + protected boolean outerJoin; + + /** + * Set the complete constraint of the query (the WHERE ... condition). + * + * @param queryConstraint the constraint + */ + public void setQueryConstraint(ConstraintImpl queryConstraint) { + this.queryConstraint = queryConstraint; + } + + /** + * Add the join condition (the ON ... condition). + * + * @param joinCondition the join condition + * @param forThisSelector if set, the join condition can only be evaluated + * when all previous selectors are executed. + */ + public void addJoinCondition(JoinConditionImpl joinCondition, boolean forThisSelector) { + if (forThisSelector) { + this.joinCondition = joinCondition; + } + allJoinConditions.add(joinCondition); + } + + /** + * Set whether this source is the right hand side of a left outer join. + * + * @param outerJoin true if yes + */ + public void setOuterJoin(boolean outerJoin) { + this.outerJoin = outerJoin; + } + + /** + * Initialize the query. This will 'wire' the selectors with the + * constraints. + * + * @param query the query + */ + public abstract void init(Query query); + + /** + * Get the selector with the given name, or null if not found. + * + * @param selectorName the selector name + * @return the selector, or null + */ + public abstract SelectorImpl getSelector(String selectorName); + + /** + * Get the selector with the given name, or fail if not found. + * + * @param selectorName the selector name + * @return the selector (never null) + */ + public SelectorImpl getExistingSelector(String selectorName) { + SelectorImpl s = getSelector(selectorName); + if (s == null) { + throw new IllegalArgumentException("Unknown selector: " + selectorName); + } + return s; + } + + /** + * Get the query plan. + * + * @param root the root + * @return the query plan + */ + public abstract String getPlan(NodeState root); + + /** + * Prepare executing the query. This method will decide which index to use. + * + */ + public abstract void prepare(); + + /** + * Execute the query. The current node is set to before the first row. + * + * @param root root state of the given revision + */ + public abstract void execute(NodeState root); + + /** + * Go to the next node for the given source. This will also filter the + * result for the right node type if required. + * + * @return true if there is a next row + */ + public abstract boolean next(); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/StaticOperandImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/StaticOperandImpl.java new file mode 100644 index 00000000000..21b060b5d30 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/StaticOperandImpl.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.query.ast; + +import org.apache.jackrabbit.oak.api.PropertyValue; + +/** + * The base class for static operands (literal, bind variables). + */ +public abstract class StaticOperandImpl extends AstElement { + + abstract PropertyValue currentValue(); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/UpperCaseImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/UpperCaseImpl.java new file mode 100644 index 00000000000..42fe512f2f7 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/UpperCaseImpl.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; + +import static org.apache.jackrabbit.oak.api.Type.STRING; + +/** + * The function "upper(..)". + */ +public class UpperCaseImpl extends DynamicOperandImpl { + + private final DynamicOperandImpl operand; + + public UpperCaseImpl(DynamicOperandImpl operand) { + this.operand = operand; + } + + public DynamicOperandImpl getOperand() { + return operand; + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return "upper(" + operand + ')'; + } + + @Override + public PropertyValue currentProperty() { + PropertyValue p = operand.currentProperty(); + if (p == null) { + return null; + } + // TODO what is the expected result of UPPER(x) for an array property? + // currently throws an exception + String value = p.getValue(STRING); + return PropertyValues.newString(value.toUpperCase()); + } + + @Override + public void restrict(FilterImpl f, Operator operator, PropertyValue v) { + // UPPER(x) implies x is not null + operand.restrict(f, Operator.NOT_EQUAL, null); + } + + @Override + public boolean canRestrictSelector(SelectorImpl s) { + return operand.canRestrictSelector(s); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/FilterImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/FilterImpl.java new file mode 100644 index 00000000000..adc44206540 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/FilterImpl.java @@ -0,0 +1,378 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.index; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map.Entry; + +import javax.jcr.PropertyType; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.query.ast.Operator; +import org.apache.jackrabbit.oak.query.ast.SelectorImpl; +import org.apache.jackrabbit.oak.spi.query.Filter; + +/** + * A filter or lookup condition. + */ +public class FilterImpl implements Filter { + + /** + * The selector this filter applies to. + */ + private final SelectorImpl selector; + + /** + * Whether the filter is always false. + */ + private boolean alwaysFalse; + + /** + * The path, or "/" (the root node, meaning no filter) if not set. + */ + private String path = "/"; + + private PathRestriction pathRestriction = PathRestriction.ALL_CHILDREN; + + /** + * The node type, or null if not set. + */ + private String nodeType; + + /** + * The fulltext search conditions, if any. + */ + private final ArrayList fulltextConditions = new ArrayList(); + + private final HashMap propertyRestrictions = + new HashMap(); + + /** + * Only return distinct values. + */ + private boolean distinct; + + // TODO support "order by" + + public FilterImpl(SelectorImpl selector) { + this.selector = selector; + } + + /** + * Get the path. + * + * @return the path + */ + @Override + public String getPath() { + return path; + } + + @Override + public PathRestriction getPathRestriction() { + return pathRestriction; + } + + public void setPath(String path) { + this.path = path; + } + + @Override + public String getNodeType() { + return nodeType; + } + + public void setNodeType(String nodeType) { + this.nodeType = nodeType; + } + + public boolean isDistinct() { + return distinct; + } + + public void setDistinct(boolean distinct) { + this.distinct = distinct; + } + + public void setAlwaysFalse() { + propertyRestrictions.clear(); + nodeType = ""; + path = "/"; + pathRestriction = PathRestriction.EXACT; + alwaysFalse = true; + } + + public boolean isAlwaysFalse() { + return alwaysFalse; + } + + public SelectorImpl getSelector() { + return selector; + } + + @Override + public Collection getPropertyRestrictions() { + return propertyRestrictions.values(); + } + + /** + * Get the restriction for the given property, if any. + * + * @param propertyName the property name + * @return the restriction or null + */ + @Override + public PropertyRestriction getPropertyRestriction(String propertyName) { + return propertyRestrictions.get(propertyName); + } + + public boolean testPath(String path) { + if (isAlwaysFalse()) { + return false; + } + switch (pathRestriction) { + case EXACT: + return path.matches(this.path); + case PARENT: + return PathUtils.isAncestor(path, this.path); + case DIRECT_CHILDREN: + return PathUtils.getParentPath(path).equals(this.path); + case ALL_CHILDREN: + return PathUtils.isAncestor(this.path, path); + default: + throw new IllegalArgumentException("Unknown path restriction: " + pathRestriction); + } + } + + public void restrictPropertyType(String propertyName, Operator operator, + int propertyType) { + if (propertyType == PropertyType.UNDEFINED) { + // not restricted + return; + } + PropertyRestriction x = propertyRestrictions.get(propertyName); + if (x == null) { + x = new PropertyRestriction(); + x.propertyName = propertyName; + propertyRestrictions.put(propertyName, x); + } + if (x.propertyType != PropertyType.UNDEFINED && x.propertyType != propertyType) { + // already restricted to another property type - always false + setAlwaysFalse(); + } + x.propertyType = propertyType; + } + + public void restrictProperty(String propertyName, Operator op, PropertyValue v) { + PropertyRestriction x = propertyRestrictions.get(propertyName); + if (x == null) { + x = new PropertyRestriction(); + x.propertyName = propertyName; + propertyRestrictions.put(propertyName, x); + } + PropertyValue oldFirst = x.first; + PropertyValue oldLast = x.last; + switch (op) { + case EQUAL: + x.first = maxValue(oldFirst, v); + x.firstIncluding = x.first == oldFirst ? x.firstIncluding : true; + x.last = minValue(oldLast, v); + x.lastIncluding = x.last == oldLast ? x.lastIncluding : true; + break; + case NOT_EQUAL: + if (v != null) { + throw new IllegalArgumentException("NOT_EQUAL only supported for NOT_EQUAL NULL"); + } + break; + case GREATER_THAN: + x.first = maxValue(oldFirst, v); + x.firstIncluding = false; + break; + case GREATER_OR_EQUAL: + x.first = maxValue(oldFirst, v); + x.firstIncluding = x.first == oldFirst ? x.firstIncluding : true; + break; + case LESS_THAN: + x.last = minValue(oldLast, v); + x.lastIncluding = false; + break; + case LESS_OR_EQUAL: + x.last = minValue(oldLast, v); + x.lastIncluding = x.last == oldLast ? x.lastIncluding : true; + break; + case LIKE: + // LIKE is handled in the fulltext index + x.isLike = true; + x.first = v; + break; + } + if (x.first != null && x.last != null) { + if (x.first.compareTo(x.last) > 0) { + setAlwaysFalse(); + } else if (x.first.compareTo(x.last) == 0 && (!x.firstIncluding || !x.lastIncluding)) { + setAlwaysFalse(); + } + } + } + + static PropertyValue maxValue(PropertyValue a, PropertyValue b) { + if (a == null) { + return b; + } + return a.compareTo(b) < 0 ? b : a; + } + + static PropertyValue minValue(PropertyValue a, PropertyValue b) { + if (a == null) { + return b; + } + return a.compareTo(b) <= 0 ? a : b; + } + + @Override + public String toString() { + StringBuilder buff = new StringBuilder(); + if (alwaysFalse) { + return "(always false)"; + } + buff.append("path: ").append(path).append(pathRestriction).append('\n'); + for (Entry p : propertyRestrictions.entrySet()) { + buff.append("property ").append(p.getKey()).append(": ").append(p.getValue()).append('\n'); + } + return buff.toString(); + } + + public void restrictPath(String addedPath, PathRestriction addedPathRestriction) { + if (addedPath == null) { + // currently unknown (prepare time) + addedPath = "/"; + } + // calculating the intersection of path restrictions + // this is ugly code, but I don't currently see a radically simpler method + switch (addedPathRestriction) { + case PARENT: + switch (pathRestriction) { + case PARENT: + // ignore as it's fast anyway + // (would need to loop to find a common ancestor) + break; + case EXACT: + case ALL_CHILDREN: + case DIRECT_CHILDREN: + if (!PathUtils.isAncestor(path, addedPath)) { + setAlwaysFalse(); + } + break; + } + pathRestriction = PathRestriction.PARENT; + path = addedPath; + break; + case EXACT: + switch (pathRestriction) { + case PARENT: + if (!PathUtils.isAncestor(addedPath, path)) { + setAlwaysFalse(); + } + break; + case EXACT: + if (!addedPath.equals(path)) { + setAlwaysFalse(); + } + break; + case ALL_CHILDREN: + if (!PathUtils.isAncestor(path, addedPath)) { + setAlwaysFalse(); + } + break; + case DIRECT_CHILDREN: + if (!PathUtils.getParentPath(addedPath).equals(path)) { + setAlwaysFalse(); + } + break; + } + path = addedPath; + pathRestriction = PathRestriction.EXACT; + break; + case ALL_CHILDREN: + switch (pathRestriction) { + case PARENT: + case EXACT: + if (!PathUtils.isAncestor(addedPath, path)) { + setAlwaysFalse(); + } + break; + case ALL_CHILDREN: + if (PathUtils.isAncestor(path, addedPath)) { + path = addedPath; + } else if (!path.equals(addedPath) && !PathUtils.isAncestor(addedPath, path)) { + setAlwaysFalse(); + } + break; + case DIRECT_CHILDREN: + if (!path.equals(addedPath) && !PathUtils.isAncestor(addedPath, path)) { + setAlwaysFalse(); + } + break; + } + break; + case DIRECT_CHILDREN: + switch (pathRestriction) { + case PARENT: + if (!PathUtils.isAncestor(addedPath, path)) { + setAlwaysFalse(); + } + break; + case EXACT: + if (!PathUtils.getParentPath(path).equals(addedPath)) { + setAlwaysFalse(); + } + break; + case ALL_CHILDREN: + if (!path.equals(addedPath) && !PathUtils.isAncestor(path, addedPath)) { + setAlwaysFalse(); + } else { + path = addedPath; + pathRestriction = PathRestriction.DIRECT_CHILDREN; + } + break; + case DIRECT_CHILDREN: + if (!path.equals(addedPath)) { + setAlwaysFalse(); + } + break; + } + break; + } + } + + @Override + public List getFulltextConditions() { + // TODO support fulltext conditions on certain properties + return fulltextConditions; + } + + public void restrictFulltextCondition(String condition) { + fulltextConditions.add(condition); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/IndexRowImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/IndexRowImpl.java new file mode 100644 index 00000000000..4a4664bbfbd --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/IndexRowImpl.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.index; + +import org.apache.jackrabbit.oak.spi.query.IndexRow; +import org.apache.jackrabbit.oak.spi.query.PropertyStateValue; + +/** + * A simple index row implementation. + */ +public class IndexRowImpl implements IndexRow { + + private final String path; + + public IndexRowImpl(String path) { + this.path = path; + } + + @Override + public String getPath() { + return path; + } + + @Override + public PropertyStateValue getValue(String columnName) { + return null; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/TraversingCursor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/TraversingCursor.java new file mode 100644 index 00000000000..573bd808803 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/TraversingCursor.java @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law + * or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.apache.jackrabbit.oak.query.index; + +import static org.apache.jackrabbit.oak.spi.query.Filter.PathRestriction.ALL_CHILDREN; + +import java.util.Deque; +import java.util.Iterator; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.plugins.memory.MemoryChildNodeEntry; +import org.apache.jackrabbit.oak.spi.query.Cursor; +import org.apache.jackrabbit.oak.spi.query.Filter; +import org.apache.jackrabbit.oak.spi.query.IndexRow; +import org.apache.jackrabbit.oak.spi.query.Filter.PathRestriction; +import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStateUtils; +import com.google.common.collect.Iterators; +import com.google.common.collect.Queues; + +/** + * A cursor that reads all nodes in a given subtree. + */ +public class TraversingCursor implements Cursor { + + private final Filter filter; + + private final Deque> nodeIterators = + Queues.newArrayDeque(); + + private String parentPath; + + private String currentPath; + + public TraversingCursor(Filter filter, NodeState root) { + this.filter = filter; + + String path = filter.getPath(); + parentPath = null; + currentPath = "/"; + NodeState parent = null; + NodeState node = root; + if (!path.equals("/")) { + for (String name : path.substring(1).split("/")) { + parentPath = currentPath; + currentPath = PathUtils.concat(parentPath, name); + + parent = node; + node = parent.getChildNode(name); + + if (node == null) { + // nothing can match this filter, leave nodes empty + return; + } + } + } + PathRestriction restriciton = filter.getPathRestriction(); + switch (restriciton) { + case EXACT: + case ALL_CHILDREN: + nodeIterators.add(Iterators.singletonIterator( + new MemoryChildNodeEntry(currentPath, node))); + parentPath = ""; + break; + case PARENT: + if (parent != null) { + nodeIterators.add(Iterators.singletonIterator( + new MemoryChildNodeEntry(parentPath, parent))); + parentPath = ""; + } + break; + case DIRECT_CHILDREN: + nodeIterators.add(node.getChildNodeEntries().iterator()); + parentPath = currentPath; + break; + default: + throw new IllegalArgumentException("Unknown restriction: " + restriciton); + } + } + + @Override + public IndexRow currentRow() { + return new IndexRowImpl(currentPath); + } + + @Override + public boolean next() { + while (!nodeIterators.isEmpty()) { + Iterator iterator = nodeIterators.getLast(); + if (iterator.hasNext()) { + ChildNodeEntry entry = iterator.next(); + NodeState node = entry.getNodeState(); + + String name = entry.getName(); + if (NodeStateUtils.isHidden(name)) { + continue; + } + currentPath = PathUtils.concat(parentPath, name); + + if (filter.getPathRestriction() == ALL_CHILDREN) { + nodeIterators.addLast(node.getChildNodeEntries().iterator()); + parentPath = currentPath; + } + return true; + } else { + nodeIterators.removeLast(); + parentPath = PathUtils.getParentPath(parentPath); + } + } + currentPath = null; + return false; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/TraversingIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/TraversingIndex.java new file mode 100644 index 00000000000..0fbd1ab79d3 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/TraversingIndex.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.index; + +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.spi.query.Cursor; +import org.apache.jackrabbit.oak.spi.query.Filter; +import org.apache.jackrabbit.oak.spi.query.QueryIndex; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * An index that traverses over a given subtree. + */ +public class TraversingIndex implements QueryIndex { + + @Override + public Cursor query(Filter filter, NodeState root) { + return new TraversingCursor(filter, root); + } + + @Override + public double getCost(Filter filter, NodeState root) { + String path = filter.getPath(); + // TODO estimate or read the node count + double nodeCount = 10000000; + if (!PathUtils.denotesRoot(path)) { + for (int depth = PathUtils.getDepth(path); depth > 0; depth--) { + // estimate 10 child nodes per node + nodeCount /= 10; + } + } + return nodeCount; + } + + @Override + public String getPlan(Filter filter, NodeState root) { + String p = filter.getPath(); + String r = filter.getPathRestriction().toString(); + if (PathUtils.denotesRoot(p)) { + p = ""; + } + return "traverse \"" + p + r + '"'; + } + + @Override + public String getIndexName() { + return "traverse"; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/OakConfiguration.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/OakConfiguration.java new file mode 100644 index 00000000000..b909be41ebe --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/OakConfiguration.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.security; + +import java.util.Collections; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; + +import org.apache.jackrabbit.oak.security.authentication.user.LoginModuleImpl; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * OakConfiguration... tmp solution missing repo-configuration in test-setup. + * TODO: remove again once OAK-17 is addressed. + */ +public class OakConfiguration extends Configuration { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(OakConfiguration.class); + + ConfigurationParameters loginConfiguration; + + public OakConfiguration() { + this(ConfigurationParameters.EMPTY); + } + + public OakConfiguration(ConfigurationParameters loginConfiguration) { + this.loginConfiguration = loginConfiguration; + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String applicationName) { + AppConfigurationEntry entry = new AppConfigurationEntry( + LoginModuleImpl.class.getName(), + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + loginConfiguration.getConfigValue(applicationName, Collections.emptyMap())); + return new AppConfigurationEntry[] {entry}; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/SecurityProviderImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/SecurityProviderImpl.java new file mode 100644 index 00000000000..ffc7af147da --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/SecurityProviderImpl.java @@ -0,0 +1,158 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.jcr.Session; +import javax.security.auth.login.Configuration; + +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.security.authentication.LoginContextProviderImpl; +import org.apache.jackrabbit.oak.security.authentication.token.TokenProviderImpl; +import org.apache.jackrabbit.oak.security.authorization.AccessControlProviderImpl; +import org.apache.jackrabbit.oak.security.principal.PrincipalManagerImpl; +import org.apache.jackrabbit.oak.security.principal.PrincipalProviderImpl; +import org.apache.jackrabbit.oak.security.privilege.PrivilegeConfigurationImpl; +import org.apache.jackrabbit.oak.security.user.UserConfigurationImpl; +import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.apache.jackrabbit.oak.spi.security.SecurityConfiguration; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +import org.apache.jackrabbit.oak.spi.security.authentication.LoginContextProvider; +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenProvider; +import org.apache.jackrabbit.oak.spi.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.oak.spi.security.principal.PrincipalConfiguration; +import org.apache.jackrabbit.oak.spi.security.principal.PrincipalProvider; +import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConfiguration; +import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration; +import org.apache.jackrabbit.oak.spi.state.NodeStore; +import org.apache.jackrabbit.oak.spi.xml.ProtectedItemImporter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SecurityProviderImpl implements SecurityProvider { + + private static final Logger log = LoggerFactory.getLogger(SecurityProviderImpl.class); + + public static final String PARAM_APP_NAME = "org.apache.jackrabbit.oak.auth.appName"; + private static final String DEFAULT_APP_NAME = "jackrabbit.oak"; + + public static final String PARAM_USER_OPTIONS = "org.apache.jackrabbit.oak.user.options"; + public static final String PARAM_TOKEN_OPTIONS = "org.apache.jackrabbit.oak.token.options"; + + private final ConfigurationParameters configuration; + + public SecurityProviderImpl() { + this(new ConfigurationParameters()); + } + + public SecurityProviderImpl(ConfigurationParameters configuration) { + this.configuration = configuration; + } + + @Nonnull + @Override + public Iterable getSecurityConfigurations() { + Set scs = new HashSet(); + scs.add(getAccessControlProvider()); + scs.add(getUserConfiguration()); + scs.add(getPrincipalConfiguration()); + scs.add(getPrivilegeConfiguration()); + return scs; + } + + @Nonnull + @Override + public LoginContextProvider getLoginContextProvider(NodeStore nodeStore, QueryIndexProvider indexProvider) { + String appName = configuration.getConfigValue(PARAM_APP_NAME, DEFAULT_APP_NAME); + Configuration loginConfig; + try { + loginConfig = Configuration.getConfiguration(); + } catch (SecurityException e) { + log.warn("Failed to retrieve login configuration: using default.", e); + loginConfig = new OakConfiguration(configuration); // TODO: define configuration structure + Configuration.setConfiguration(loginConfig); + } + return new LoginContextProviderImpl(appName, loginConfig, nodeStore, indexProvider, this); + } + + @Nonnull + @Override + public TokenProvider getTokenProvider(Root root) { + ConfigurationParameters options = configuration.getConfigValue(PARAM_TOKEN_OPTIONS, new ConfigurationParameters()); + return new TokenProviderImpl(root, options, getUserConfiguration()); + } + + @Nonnull + @Override + public AccessControlProvider getAccessControlProvider() { + return new AccessControlProviderImpl(); + } + + @Nonnull + @Override + public PrivilegeConfiguration getPrivilegeConfiguration() { + return new PrivilegeConfigurationImpl(); + } + + @Nonnull + @Override + public UserConfiguration getUserConfiguration() { + ConfigurationParameters options = configuration.getConfigValue(PARAM_USER_OPTIONS, new ConfigurationParameters()); + return new UserConfigurationImpl(options, this); + } + + @Nonnull + @Override + public PrincipalConfiguration getPrincipalConfiguration() { + return new PrincipalConfigurationImpl(); + } + + private class PrincipalConfigurationImpl extends SecurityConfiguration.Default implements PrincipalConfiguration { + @Nonnull + @Override + public PrincipalManager getPrincipalManager(Session session, Root root, NamePathMapper namePathMapper) { + PrincipalProvider principalProvider = getPrincipalProvider(root, namePathMapper); + return new PrincipalManagerImpl(principalProvider); + } + + @Nonnull + @Override + public PrincipalProvider getPrincipalProvider(Root root, NamePathMapper namePathMapper) { + return new PrincipalProviderImpl(root, getUserConfiguration(), namePathMapper); + } + + @Nonnull + @Override + public List getValidatorProviders() { + return Collections.emptyList(); + } + + @Nonnull + @Override + public List getProtectedItemImporters() { + return Collections.emptyList(); + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/AuthInfoImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/AuthInfoImpl.java new file mode 100644 index 00000000000..483e4c95749 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/AuthInfoImpl.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authentication; + +import java.security.Principal; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.AuthInfo; + +/** + * Default implementation of the AuthInfo interface. + */ +public class AuthInfoImpl implements AuthInfo { + + private final String userID; + private final Map attributes; + private final Set principals; + + public AuthInfoImpl(String userID, Map attributes, Set principals) { + this.userID = userID; + this.attributes = (attributes == null) ? Collections.emptyMap() : attributes; + this.principals = Collections.unmodifiableSet(principals); + } + + //-----------------------------------------------------------< AuthInfo >--- + @Override + public String getUserID() { + return userID; + } + + @Nonnull + @Override + public String[] getAttributeNames() { + return attributes.keySet().toArray(new String[attributes.size()]); + } + + @Override + public Object getAttribute(String attributeName) { + return attributes.get(attributeName); + } + + @Override + public Set getPrincipals() { + return principals; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/CallbackHandlerImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/CallbackHandlerImpl.java new file mode 100644 index 00000000000..009e9785cef --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/CallbackHandlerImpl.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.security.authentication; + +import java.io.IOException; +import javax.jcr.Credentials; +import javax.jcr.SimpleCredentials; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; + +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +import org.apache.jackrabbit.oak.spi.security.authentication.callback.CredentialsCallback; +import org.apache.jackrabbit.oak.spi.security.authentication.callback.RepositoryCallback; +import org.apache.jackrabbit.oak.spi.security.authentication.callback.SecurityProviderCallback; +import org.apache.jackrabbit.oak.spi.state.NodeStore; + +/** + * Default implementation of the {@link CallbackHandler} interface. It currently + * supports the following {@code Callback} implementations: + * + *
    + *
  • {@link CredentialsCallback}
  • + *
  • {@link NameCallback}
  • + *
  • {@link PasswordCallback}
  • + *
  • {@link SecurityProviderCallback}
  • + *
  • {@link RepositoryCallback}
  • + *
+ */ +public class CallbackHandlerImpl implements CallbackHandler { + + private final Credentials credentials; + private final String workspaceName; + private final NodeStore nodeStore; + private final QueryIndexProvider indexProvider; + private final SecurityProvider securityProvider; + + public CallbackHandlerImpl(Credentials credentials, String workspaceName, + NodeStore nodeStore, QueryIndexProvider indexProvider, + SecurityProvider securityProvider) { + this.credentials = credentials; + this.workspaceName = workspaceName; + this.nodeStore = nodeStore; + this.indexProvider = indexProvider; + this.securityProvider = securityProvider; + } + + //----------------------------------------------------< CallbackHandler >--- + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof CredentialsCallback) { + ((CredentialsCallback) callback).setCredentials(credentials); + } else if (callback instanceof NameCallback) { + ((NameCallback) callback).setName(getName()); + } else if (callback instanceof PasswordCallback) { + ((PasswordCallback) callback).setPassword(getPassword()); + } else if (callback instanceof SecurityProviderCallback) { + ((SecurityProviderCallback) callback).setSecurityProvider(securityProvider); + } else if (callback instanceof RepositoryCallback) { + RepositoryCallback repositoryCallback = (RepositoryCallback) callback; + repositoryCallback.setNodeStore(nodeStore); + repositoryCallback.setIndexProvider(indexProvider); + repositoryCallback.setWorkspaceName(workspaceName); + } else { + throw new UnsupportedCallbackException(callback); + } + } + } + + //------------------------------------------------------------< private >--- + + private String getName(){ + if (credentials instanceof SimpleCredentials) { + return ((SimpleCredentials) credentials).getUserID(); + } else { + return null; + } + } + + private char[] getPassword() { + if (credentials instanceof SimpleCredentials) { + return ((SimpleCredentials) credentials).getPassword(); + } else { + return null; + } + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/LoginContextProviderImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/LoginContextProviderImpl.java new file mode 100644 index 00000000000..05d37d7d97c --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/LoginContextProviderImpl.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.security.authentication; + +import java.security.AccessController; +import javax.annotation.Nonnull; +import javax.jcr.Credentials; +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +import org.apache.jackrabbit.oak.spi.security.authentication.JaasLoginContext; +import org.apache.jackrabbit.oak.spi.security.authentication.LoginContext; +import org.apache.jackrabbit.oak.spi.security.authentication.LoginContextProvider; +import org.apache.jackrabbit.oak.spi.state.NodeStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@code LoginContextProvider} + */ +public class LoginContextProviderImpl implements LoginContextProvider { + + private static final Logger log = LoggerFactory.getLogger(LoginContextProviderImpl.class); + + private final String appName; + private final Configuration configuration; + private final NodeStore nodeStore; + private final QueryIndexProvider indexProvider; + private final SecurityProvider securityProvider; + + public LoginContextProviderImpl(String appName, Configuration configuration, + NodeStore nodeStore, QueryIndexProvider indexProvider, + SecurityProvider securityProvider) { + this.appName = appName; + this.configuration = configuration; + this.nodeStore = nodeStore; + this.indexProvider = indexProvider; + this.securityProvider = securityProvider; + } + + @Override + @Nonnull + public LoginContext getLoginContext(Credentials credentials, String workspaceName) + throws LoginException { + Subject subject = getSubject(); + CallbackHandler handler = getCallbackHandler(credentials, workspaceName); + return new JaasLoginContext(appName, subject, handler, configuration); + } + + //------------------------------------------------------------< private >--- + private static Subject getSubject() { + Subject subject = null; + try { + subject = Subject.getSubject(AccessController.getContext()); + } catch (SecurityException e) { + log.debug("Can't check for pre-authentication. Reason:", e.getMessage()); + } + if (subject == null) { + subject = new Subject(); + } + return subject; + } + + private CallbackHandler getCallbackHandler(Credentials credentials, String workspaceName) { + return new CallbackHandlerImpl(credentials, workspaceName, nodeStore, indexProvider, securityProvider); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenAuthentication.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenAuthentication.java new file mode 100644 index 00000000000..991a1c01870 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenAuthentication.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authentication.token; + +import java.util.Date; +import javax.annotation.Nonnull; +import javax.jcr.Credentials; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; +import org.apache.jackrabbit.oak.spi.security.authentication.Authentication; +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenInfo; +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of the {@code Authentication} interface that deals with + * token based login. {@link #authenticate(javax.jcr.Credentials) Authentication} + * will be successful if the specified credentials are valid {@link TokenCredentials} + * according to the characteristics and constraints enforced by {@link TokenProvider} + * and the information obtained using {@link TokenProvider#getTokenInfo(String)} + * respectively. + */ +class TokenAuthentication implements Authentication { + + private static final Logger log = LoggerFactory.getLogger(TokenAuthentication.class); + + private final TokenProvider tokenProvider; + private TokenInfo tokenInfo; + + TokenAuthentication(TokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } + + //-----------------------------------------------------< Authentication >--- + @Override + public boolean authenticate(Credentials credentials) throws LoginException { + if (tokenProvider != null && credentials instanceof TokenCredentials) { + TokenCredentials tc = (TokenCredentials) credentials; + if (!validateCredentials(tc)) { + throw new LoginException("Invalid token credentials."); + } else { + return true; + } + } + // no tokenProvider or other credentials implementation -> not handled here. + return false; + } + + //-----------------------------------------------------------< internal >--- + @Nonnull + TokenInfo getTokenInfo() { + if (tokenInfo == null) { + throw new IllegalStateException("Token info can only be retrieved after successful authentication."); + } + return tokenInfo; + } + + //------------------------------------------------------------< private >--- + private boolean validateCredentials(TokenCredentials tokenCredentials) { + // credentials without userID -> check if attributes provide + // sufficient information for successful authentication. + String token = tokenCredentials.getToken(); + + tokenInfo = tokenProvider.getTokenInfo(token); + if (tokenInfo == null) { + log.debug("No valid TokenInfo for token."); + return false; + } + + long loginTime = new Date().getTime(); + if (tokenInfo.isExpired(loginTime)) { + // token is expired + log.debug("Token is expired"); + tokenProvider.removeToken(tokenInfo); + return false; + } + + if (tokenInfo.matches(tokenCredentials)) { + tokenProvider.resetTokenExpiration(tokenInfo, loginTime); + return true; + } + + return false; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenLoginModule.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenLoginModule.java new file mode 100644 index 00000000000..94aa90f1d81 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenLoginModule.java @@ -0,0 +1,248 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authentication.token; + +import java.io.IOException; +import java.security.Principal; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.jcr.Credentials; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; +import org.apache.jackrabbit.oak.api.AuthInfo; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.security.authentication.AuthInfoImpl; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +import org.apache.jackrabbit.oak.spi.security.authentication.AbstractLoginModule; +import org.apache.jackrabbit.oak.spi.security.authentication.callback.TokenProviderCallback; +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenInfo; +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@code LoginModule} implementation that is able to handle login request + * based on {@link TokenCredentials}. In combination with another login module + * that handles other {@code Credentials} implementation this module will also + * take care of creating new login tokens and the corresponding credentials + * upon {@link #commit()}that it will be able to deal with in subsequent + * login calls. + * + *

Login and Commit

+ *

Login

+ * This {@code LoginModule} implementation performs the following tasks upon + * {@link #login()}. + * + *
    + *
  1. Try to retrieve {@link TokenCredentials} credentials (see also + * {@link AbstractLoginModule#getCredentials()})
  2. + *
  3. Validates the credentials based on the functionality provided by + * {@link TokenAuthentication#authenticate(javax.jcr.Credentials)}
  4. + *
  5. Upon success it retrieves {@code userId} from the {@link TokenInfo} + * and calculates the principals associated with that user,
  6. + *
  7. and finally puts the credentials on the shared state.
  8. + *
+ * + * If no {@code TokenProvider} has been configured {@link #login()} or if + * no {@code TokenCredentials} can be obtained this module will return {@code false}. + * + *

Commit

+ * If login was successfully handled by this module the {@link #commit()} will + * just populate the subject.

+ * + * If the login was successfully handled by another module in the chain, the + * {@code TokenLoginModule} will test if the login was associated with a + * request for login token generation. This mandates that there are credentials + * present on the shared state that fulfill the requirements defined by + * {@link TokenProvider#doCreateToken(javax.jcr.Credentials)}. + * + *

Example Configurations

+ * The authentication configuration using this {@code LoginModule} could for + * example look as follows: + * + *

TokenLoginModule in combination with another LoginModule

+ *
+ *    jackrabbit.oak {
+ *            org.apache.jackrabbit.oak.security.authentication.token.TokenLoginModule sufficient;
+ *            org.apache.jackrabbit.oak.security.authentication.user.LoginModuleImpl required;
+ *    };
+ * 
+ * In this case the TokenLoginModule would handle any login issued with + * {@link TokenCredentials} while the second module would take care any other + * credentials implementations as long they are supported by the module. In + * addition the {@link TokenLoginModule} will issue a new token if the login + * succeeded and the credentials provided by the shared state can be used + * to issue a new login token (see {@link TokenProvider#doCreateToken(javax.jcr.Credentials)}. + * + *

TokenLoginModule as single way to login

+ *
+ *    jackrabbit.oak {
+ *            org.apache.jackrabbit.oak.security.authentication.token.TokenLoginModule required;
+ *    };
+ * 
+ * If the {@code TokenLoginModule} as single entry in the login configuration + * the login token must be generated by the application by calling + * {@link TokenProvider#createToken(Credentials)} or + * {@link TokenProvider#createToken(String, java.util.Map)}. + */ +public final class TokenLoginModule extends AbstractLoginModule { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(TokenLoginModule.class); + + private TokenProvider tokenProvider; + + private TokenCredentials tokenCredentials; + private TokenInfo tokenInfo; + private String userId; + private Set principals; + + //--------------------------------------------------------< LoginModule >--- + @Override + public boolean login() throws LoginException { + tokenProvider = getTokenProvider(); + if (tokenProvider == null) { + return false; + } + + Credentials credentials = getCredentials(); + if (credentials instanceof TokenCredentials) { + TokenCredentials tc = (TokenCredentials) credentials; + TokenAuthentication authentication = new TokenAuthentication(tokenProvider); + if (authentication.authenticate(tc)) { + tokenCredentials = tc; + tokenInfo = authentication.getTokenInfo(); + userId = tokenInfo.getUserId(); + principals = getPrincipals(userId); + + log.debug("Login: adding login name to shared state."); + sharedState.put(SHARED_KEY_LOGIN_NAME, userId); + return true; + } + } + + return false; + } + + @Override + public boolean commit() { + if (tokenCredentials != null) { + if (!subject.isReadOnly()) { + subject.getPublicCredentials().add(tokenCredentials); + subject.getPrincipals().addAll(principals); + subject.getPublicCredentials().add(getAuthInfo(tokenInfo)); + } + return true; + } + + // the login attempt on this module did not succeed: clear state + // and check if another successful login asks for a new token to be created. + clearState(); + + if (tokenProvider != null && sharedState.containsKey(SHARED_KEY_CREDENTIALS)) { + Credentials shared = getSharedCredentials(); + if (shared != null && tokenProvider.doCreateToken(shared)) { + TokenInfo ti = tokenProvider.createToken(shared); + if (ti != null) { + TokenCredentials tc = new TokenCredentials(ti.getToken()); + Map attributes = ti.getPrivateAttributes(); + for (String name : attributes.keySet()) { + tc.setAttribute(name, attributes.get(name)); + } + attributes = ti.getPublicAttributes(); + for (String name : attributes.keySet()) { + tc.setAttribute(name, attributes.get(name)); + } + subject.getPublicCredentials().add(tc); + } + } + } + return false; + } + + //------------------------------------------------< AbstractLoginModule >--- + @Override + protected Set getSupportedCredentials() { + return Collections.singleton(TokenCredentials.class); + } + + @Override + protected void clearState() { + super.clearState(); + + tokenCredentials = null; + tokenInfo = null; + userId = null; + principals = null; + } + + //------------------------------------------------------------< private >--- + + /** + * Retrieve the token provider + * @return the token provider or {@code null}. + */ + @CheckForNull + private TokenProvider getTokenProvider() { + TokenProvider provider = null; + SecurityProvider securityProvider = getSecurityProvider(); + Root root = getRoot(); + if (root != null && securityProvider != null) { + provider = securityProvider.getTokenProvider(root); + } + if (provider == null && callbackHandler != null) { + try { + TokenProviderCallback tcCallback = new TokenProviderCallback(); + callbackHandler.handle(new Callback[] {tcCallback}); + provider = tcCallback.getTokenProvider(); + } catch (IOException e) { + log.warn(e.getMessage()); + } catch (UnsupportedCallbackException e) { + log.warn(e.getMessage()); + } + } + return provider; + } + + /** + * Create the {@code AuthInfo} for the specified {@code tokenInfo} as well as + * userId and principals, that have been set upon {@link #login}. + * + * @param tokenInfo The tokenInfo to retrieve attributes from. + * @return The {@code AuthInfo} resulting from the successful login. + */ + @Nonnull + private AuthInfo getAuthInfo(TokenInfo tokenInfo) { + Map attributes = new HashMap(); + if (tokenProvider != null && tokenInfo != null) { + Map publicAttributes = tokenInfo.getPublicAttributes(); + for (String attrName : publicAttributes.keySet()) { + attributes.put(attrName, publicAttributes.get(attrName)); + } + } + return new AuthInfoImpl(userId, attributes, principals); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProviderImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProviderImpl.java new file mode 100644 index 00000000000..d20cc85c4b3 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProviderImpl.java @@ -0,0 +1,458 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authentication.token; + +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.SimpleCredentials; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.api.CommitFailedException; +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.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager; +import org.apache.jackrabbit.oak.plugins.name.NamespaceConstants; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.apache.jackrabbit.oak.spi.security.authentication.ImpersonationCredentials; +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenInfo; +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenProvider; +import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration; +import org.apache.jackrabbit.oak.spi.security.user.util.PasswordUtility; +import org.apache.jackrabbit.oak.util.NodeUtil; +import org.apache.jackrabbit.util.ISO8601; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.jackrabbit.oak.api.Type.STRING; + +/** + * Default implementation of the {@code TokenProvider} interface with the + * following characteristics. + * + *

doCreateToken

+ * The {@link #doCreateToken(javax.jcr.Credentials)} returns {@code true} if + * {@code SimpleCredentials} can be extracted from the specified credentials + * object and that simple credentials object has a {@link #TOKEN_ATTRIBUTE} + * attribute with an empty value. + * + *

createToken

+ * This implementation of {@link #createToken(javax.jcr.Credentials)} will + * create a separate token node underneath the user home node. That token + * node contains the hashed token, the expiration time and additional + * mandatory attributes that will be verified during login. + */ +public class TokenProviderImpl implements TokenProvider { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(TokenProviderImpl.class); + + /** + * Constant for the token attribute passed with valid simple credentials to + * trigger the generation of a new token. + */ + private static final String TOKEN_ATTRIBUTE = ".token"; + private static final String TOKEN_ATTRIBUTE_EXPIRY = "rep:token.exp"; + private static final String TOKEN_ATTRIBUTE_KEY = "rep:token.key"; + private static final String TOKENS_NODE_NAME = ".tokens"; + private static final String TOKENS_NT_NAME = JcrConstants.NT_UNSTRUCTURED; + private static final String TOKEN_NT_NAME = "rep:Token"; + + /** + * Default expiration time in ms for login tokens is 2 hours. + */ + private static final long DEFAULT_TOKEN_EXPIRATION = 2 * 3600 * 1000; + private static final int DEFAULT_KEY_SIZE = 8; + private static final char DELIM = '_'; + + private static final Set RESERVED_ATTRIBUTES = new HashSet(2); + static { + RESERVED_ATTRIBUTES.add(TOKEN_ATTRIBUTE); + RESERVED_ATTRIBUTES.add(TOKEN_ATTRIBUTE_EXPIRY); + RESERVED_ATTRIBUTES.add(TOKEN_ATTRIBUTE_KEY); + } + + private final Root root; + private final ConfigurationParameters options; + + private final long tokenExpiration; + private final UserManager userManager; + private final IdentifierManager identifierManager; + + public TokenProviderImpl(Root root, ConfigurationParameters options, UserConfiguration userConfiguration) { + this.root = root; + this.options = options; + + this.tokenExpiration = options.getConfigValue(PARAM_TOKEN_EXPIRATION, Long.valueOf(DEFAULT_TOKEN_EXPIRATION)); + this.userManager = userConfiguration.getUserManager(root, NamePathMapper.DEFAULT); + this.identifierManager = new IdentifierManager(root); + } + + //------------------------------------------------------< TokenProvider >--- + @Override + public boolean doCreateToken(Credentials credentials) { + SimpleCredentials sc = extractSimpleCredentials(credentials); + if (sc == null) { + return false; + } else { + Object attr = sc.getAttribute(TOKEN_ATTRIBUTE); + return (attr != null && "".equals(attr.toString())); + } + } + + @Override + public TokenInfo createToken(Credentials credentials) { + SimpleCredentials sc = extractSimpleCredentials(credentials); + TokenInfo tokenInfo = null; + if (sc != null) { + String[] attrNames = sc.getAttributeNames(); + Map attributes = new HashMap(attrNames.length); + for (String attrName : sc.getAttributeNames()) { + attributes.put(attrName, sc.getAttribute(attrName).toString()); + } + tokenInfo = createToken(sc.getUserID(), attributes); + if (tokenInfo != null) { + // also set the new token to the simple credentials. + sc.setAttribute(TOKEN_ATTRIBUTE, tokenInfo.getToken()); + } + } + + return tokenInfo; + } + + @Override + public TokenInfo createToken(String userId, Map attributes) { + try { + Authorizable user = userManager.getAuthorizable(userId); + if (user != null && !user.isGroup()) { + NodeUtil userNode = new NodeUtil(root.getTree(user.getPath())); + NodeUtil tokenParent = userNode.getChild(TOKENS_NODE_NAME); + if (tokenParent == null) { + tokenParent = userNode.addChild(TOKENS_NODE_NAME, TOKENS_NT_NAME); + } + + long creationTime = new Date().getTime(); + Calendar creation = GregorianCalendar.getInstance(); + creation.setTimeInMillis(creationTime); + String tokenName = Text.replace(ISO8601.format(creation), ":", "."); + + NodeUtil tokenNode = tokenParent.addChild(tokenName, TOKEN_NT_NAME); + tokenNode.setString(JcrConstants.JCR_UUID, IdentifierManager.generateUUID()); + + String key = generateKey(options.getConfigValue(PARAM_TOKEN_LENGTH, DEFAULT_KEY_SIZE)); + String nodeId = identifierManager.getIdentifier(tokenNode.getTree()); + String token = new StringBuilder(nodeId).append(DELIM).append(key).toString(); + + String keyHash = PasswordUtility.buildPasswordHash(key); + tokenNode.setString(TOKEN_ATTRIBUTE_KEY, keyHash); + final long expirationTime = creationTime + tokenExpiration; + tokenNode.setDate(TOKEN_ATTRIBUTE_EXPIRY, expirationTime); + + for (String name : attributes.keySet()) { + if (!RESERVED_ATTRIBUTES.contains(name)) { + String attr = attributes.get(name).toString(); + tokenNode.setString(name, attr); + } + } + root.commit(); + + return new TokenInfoImpl(tokenNode, token, userId); + } else { + log.debug("Cannot create login token: No corresponding node for User " + userId + '.'); + } + + } catch (NoSuchAlgorithmException e) { + log.debug("Failed to create login token ", e.getMessage()); + } catch (UnsupportedEncodingException e) { + log.debug("Failed to create login token ", e.getMessage()); + } catch (CommitFailedException e) { + log.debug("Failed to create login token ", e.getMessage()); + } catch (RepositoryException e) { + log.debug("Failed to create login token ", e.getMessage()); + } + + return null; + } + + @Override + public TokenInfo getTokenInfo(String token) { + int pos = token.indexOf(DELIM); + String nodeId = (pos == -1) ? token : token.substring(0, pos); + Tree tokenTree = identifierManager.getTree(nodeId); + String userId = getUserId(tokenTree); + if (tokenTree == null || userId == null) { + return null; + } else { + return new TokenInfoImpl(new NodeUtil(tokenTree), token, userId); + } + } + + @Override + public boolean removeToken(TokenInfo tokenInfo) { + Tree tokenTree = getTokenTree(tokenInfo); + if (tokenTree != null) { + try { + if (tokenTree.remove()) { + root.commit(); + return true; + } + } catch (CommitFailedException e) { + log.debug("Error while removing expired token", e.getMessage()); + } + } + return false; + } + + @Override + public boolean resetTokenExpiration(TokenInfo tokenInfo, long loginTime) { + Tree tokenTree = getTokenTree(tokenInfo); + if (tokenTree != null) { + NodeUtil tokenNode = new NodeUtil(tokenTree); + long expTime = getExpirationTime(tokenNode, 0); + if (tokenInfo.isExpired(loginTime)) { + log.debug("Attempt to reset an expired token."); + return false; + } + + if (expTime - loginTime <= tokenExpiration/2) { + long expirationTime = loginTime + tokenExpiration; + try { + tokenNode.setDate(TOKEN_ATTRIBUTE_EXPIRY, expirationTime); + root.commit(); + log.debug("Successfully reset token expiration time."); + return true; + } catch (CommitFailedException e) { + log.warn("Error while resetting token expiration", e.getMessage()); + } + } + } + return false; + } + + + //-------------------------------------------------------------------------- + + private static long getExpirationTime(NodeUtil tokenNode, long defaultValue) { + return tokenNode.getLong(TOKEN_ATTRIBUTE_EXPIRY, defaultValue); + } + + @CheckForNull + private static SimpleCredentials extractSimpleCredentials(Credentials credentials) { + if (credentials instanceof SimpleCredentials) { + return (SimpleCredentials) credentials; + } + + if (credentials instanceof ImpersonationCredentials) { + Credentials base = ((ImpersonationCredentials) credentials).getBaseCredentials(); + if (base instanceof SimpleCredentials) { + return (SimpleCredentials) base; + } + } + + // cannot extract SimpleCredentials + return null; + } + + @Nonnull + private static String generateKey(int size) { + SecureRandom random = new SecureRandom(); + byte key[] = new byte[size]; + random.nextBytes(key); + + StringBuilder res = new StringBuilder(key.length * 2); + for (byte b : key) { + res.append(Text.hexTable[(b >> 4) & 15]); + res.append(Text.hexTable[b & 15]); + } + return res.toString(); + } + + @CheckForNull + private Tree getTokenTree(TokenInfo tokenInfo) { + if (tokenInfo instanceof TokenInfoImpl) { + return root.getTree(((TokenInfoImpl) tokenInfo).tokenPath); + } else { + return null; + } + } + + @CheckForNull + private String getUserId(Tree tokenTree) { + if (tokenTree != null) { + try { + String userPath = Text.getRelativeParent(tokenTree.getPath(), 2); + Authorizable authorizable = userManager.getAuthorizableByPath(userPath); + if (authorizable != null && !authorizable.isGroup() && !((User) authorizable).isDisabled()) { + return authorizable.getID(); + } + } catch (RepositoryException e) { + log.debug("Cannot determine userID from token: ", e.getMessage()); + } + } + return null; + } + + //-------------------------------------------------------------------------- + /** + * TokenInfo + */ + private static class TokenInfoImpl implements TokenInfo { + + private final String token; + private final String tokenPath; + private final String userId; + + private final long expirationTime; + private final String key; + + private final Map mandatoryAttributes; + private final Map publicAttributes; + + + private TokenInfoImpl(NodeUtil tokenNode, String token, String userId) { + this.token = token; + this.tokenPath = tokenNode.getTree().getPath(); + this.userId = userId; + + expirationTime = getExpirationTime(tokenNode, Long.MIN_VALUE); + key = tokenNode.getString(TOKEN_ATTRIBUTE_KEY, null); + + mandatoryAttributes = new HashMap(); + publicAttributes = new HashMap(); + for (PropertyState propertyState : tokenNode.getTree().getProperties()) { + String name = propertyState.getName(); + String value = propertyState.getValue(STRING); + if (RESERVED_ATTRIBUTES.contains(name)) { + continue; + } + if (isMandatoryAttribute(name)) { + mandatoryAttributes.put(name, value); + } else if (isInfoAttribute(name)) { + // info attribute + publicAttributes.put(name, value); + } // else: jcr specific property + } + } + + //------------------------------------------------------< TokenInfo >--- + + @Override + public String getUserId() { + return userId; + } + + @Override + public String getToken() { + return token; + } + + @Override + public boolean isExpired(long loginTime) { + return expirationTime < loginTime; + } + + @Override + public boolean matches(TokenCredentials tokenCredentials) { + String token = tokenCredentials.getToken(); + int pos = token.lastIndexOf(DELIM); + if (pos > -1) { + token = token.substring(pos + 1); + } + if (key == null || !PasswordUtility.isSame(key, token)) { + return false; + } + + for (String name : mandatoryAttributes.keySet()) { + String expectedValue = mandatoryAttributes.get(name); + if (!expectedValue.equals(tokenCredentials.getAttribute(name))) { + return false; + } + } + + // update set of informative attributes on the credentials + // based on the properties present on the token node. + Collection attrNames = Arrays.asList(tokenCredentials.getAttributeNames()); + for (String name : publicAttributes.keySet()) { + if (!attrNames.contains(name)) { + tokenCredentials.setAttribute(name, publicAttributes.get(name).toString()); + + } + } + return true; + } + + @Override + public Map getPrivateAttributes() { + return Collections.unmodifiableMap(mandatoryAttributes); + } + + @Override + public Map getPublicAttributes() { + return Collections.unmodifiableMap(publicAttributes); + } + + /** + * Returns {@code true} if the specified {@code attributeName} + * starts with or equals {@link #TOKEN_ATTRIBUTE}. + * + * @param attributeName + * @return {@code true} if the specified {@code attributeName} + * starts with or equals {@link #TOKEN_ATTRIBUTE}. + */ + private static boolean isMandatoryAttribute(String attributeName) { + return attributeName != null && attributeName.startsWith(TOKEN_ATTRIBUTE); + } + + /** + * Returns {@code false} if the specified attribute name doesn't have + * a 'jcr' or 'rep' namespace prefix; {@code true} otherwise. This is + * a lazy evaluation in order to avoid testing the defining node type of + * the associated jcr property. + * + * @param propertyName + * @return {@code true} if the specified property name doesn't seem + * to represent repository internal information. + */ + private static boolean isInfoAttribute(String propertyName) { + String prefix = Text.getNamespacePrefix(propertyName); + return !NamespaceConstants.RESERVED_PREFIXES.contains(prefix); + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/user/LoginModuleImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/user/LoginModuleImpl.java new file mode 100644 index 00000000000..38601741d45 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/user/LoginModuleImpl.java @@ -0,0 +1,220 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authentication.user; + +import java.io.IOException; +import java.security.Principal; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import javax.annotation.CheckForNull; +import javax.jcr.Credentials; +import javax.jcr.GuestCredentials; +import javax.jcr.SimpleCredentials; +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.oak.api.AuthInfo; +import org.apache.jackrabbit.oak.security.authentication.AuthInfoImpl; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +import org.apache.jackrabbit.oak.spi.security.authentication.AbstractLoginModule; +import org.apache.jackrabbit.oak.spi.security.authentication.Authentication; +import org.apache.jackrabbit.oak.spi.security.authentication.ImpersonationCredentials; +import org.apache.jackrabbit.oak.spi.security.principal.PrincipalProvider; +import org.apache.jackrabbit.oak.spi.security.user.util.UserUtility; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Default login module implementation that authenticates JCR {@code Credentials} + * against the repository. Based on the credentials the {@link Principal}s + * associated with user are retrieved from a configurable {@link PrincipalProvider}. + * + *

Credentials

+ * + * The {@code Credentials} are collected during {@link #login()} using the + * following logic: + * + *
    + *
  • {@code Credentials} as specified in {@link javax.jcr.Repository#login(javax.jcr.Credentials)} + * in which case they are retrieved from the {@code CallbackHandler}.
  • + *
  • A {@link #SHARED_KEY_CREDENTIALS} entry in the shared state. The + * expected value is a validated single {@code Credentials} object.
  • + *
  • If neither of the above variants provides Credentials this module + * tries to obtain them from the subject. See also + * {@link Subject#getSubject(java.security.AccessControlContext)}
  • + *
+ * + * This implementation of the {@code LoginModule} currently supports the following + * types of JCR Credentials: + * + *
    + *
  • {@link SimpleCredentials}
  • + *
  • {@link GuestCredentials}
  • + *
  • {@link ImpersonationCredentials}
  • + *
+ * + * The {@link Credentials} obtained during the {@code #login()} are added to + * the shared state and - upon successful {@code #commit()} to the {@code Subject}. + * + *

Principals

+ * Upon successful login the principals associated with the user are calculated + * (see also {@link AbstractLoginModule#getPrincipals(String)}. These principals + * are finally added to the subject during {@code #commit()}. + * + *

Impersonation

+ * Impersonation such as defined by {@link javax.jcr.Session#impersonate(javax.jcr.Credentials)} + * is covered by this login module by the means of {@link ImpersonationCredentials}. + * Impersonation will succeed if the {@link ImpersonationCredentials#getBaseCredentials() base credentials} + * refer to a valid user that has not been disabled. If the authenticating + * subject is not allowed to impersonate the specified user, the login attempt + * will fail with {@code LoginException}.

+ * Please note, that a user will always be allowed to impersonate him/herself + * irrespective of the impersonation definitions exposed by + * {@link org.apache.jackrabbit.api.security.user.User#getImpersonation()} + */ +public final class LoginModuleImpl extends AbstractLoginModule { + + private static final Logger log = LoggerFactory.getLogger(LoginModuleImpl.class); + + protected static final Set SUPPORTED_CREDENTIALS = new HashSet(3); + static { + SUPPORTED_CREDENTIALS.add(SimpleCredentials.class); + SUPPORTED_CREDENTIALS.add(GuestCredentials.class); + SUPPORTED_CREDENTIALS.add(ImpersonationCredentials.class); + } + + private Credentials credentials; + private Set principals; + private String userId; + + //--------------------------------------------------------< LoginModule >--- + + @Override + public boolean login() throws LoginException { + credentials = getCredentials(); + userId = getUserId(); + + if (credentials == null || userId == null) { + log.debug("Could not extract userId/credentials"); + return false; + } + + Authentication authentication = new UserAuthentication(userId, getUserManager()); + boolean success = authentication.authenticate(credentials); + if (success) { + principals = getPrincipals(userId); + + log.debug("Adding Credentials to shared state."); + sharedState.put(SHARED_KEY_CREDENTIALS, credentials); + + log.debug("Adding login name to shared state."); + sharedState.put(SHARED_KEY_LOGIN_NAME, userId); + } + return success; + } + + @Override + public boolean commit() { + if (credentials == null || principals == null) { + // login attempt in this login module was not successful + clearState(); + return false; + } else { + if (!subject.isReadOnly()) { + subject.getPrincipals().addAll(principals); + subject.getPublicCredentials().add(credentials); + subject.getPublicCredentials().add(createAuthInfo()); + } else { + log.debug("Could not add information to read only subject {}", subject); + } + return true; + } + } + + //------------------------------------------------< AbstractLoginModule >--- + @Override + protected Set getSupportedCredentials() { + return SUPPORTED_CREDENTIALS; + } + + @Override + protected void clearState() { + super.clearState(); + + credentials = null; + principals = null; + userId = null; + } + + //-------------------------------------------------------------------------- + @CheckForNull + private String getUserId() { + String uid = null; + if (credentials != null) { + if (credentials instanceof SimpleCredentials) { + uid = ((SimpleCredentials) credentials).getUserID(); + } else if (credentials instanceof GuestCredentials) { + uid = getAnonymousId(); + } else if (credentials instanceof ImpersonationCredentials) { + Credentials bc = ((ImpersonationCredentials) credentials).getBaseCredentials(); + if (bc instanceof SimpleCredentials) { + uid = ((SimpleCredentials) bc).getUserID(); + } + } else { + try { + NameCallback callback = new NameCallback("User-ID: "); + callbackHandler.handle(new Callback[]{callback}); + uid = callback.getName(); + } catch (UnsupportedCallbackException e) { + log.warn("Credentials- or NameCallback must be supported"); + } catch (IOException e) { + log.error("Name-Callback failed: " + e.getMessage()); + } + } + } + + if (uid == null) { + uid = getSharedLoginName(); + } + return uid; + } + + private String getAnonymousId() { + SecurityProvider sp = getSecurityProvider(); + if (sp == null) { + return null; + } else { + return UserUtility.getAnonymousId(sp.getUserConfiguration().getConfigurationParameters()); + } + } + + private AuthInfo createAuthInfo() { + Map attributes = new HashMap(); + if (credentials instanceof SimpleCredentials) { + SimpleCredentials sc = (SimpleCredentials) credentials; + for (String attrName : sc.getAttributeNames()) { + attributes.put(attrName, sc.getAttribute(attrName)); + } + } + return new AuthInfoImpl(userId, attributes, principals); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/user/UserAuthentication.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/user/UserAuthentication.java new file mode 100644 index 00000000000..31c3a349196 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/user/UserAuthentication.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authentication.user; + +import java.util.Collections; +import javax.jcr.Credentials; +import javax.jcr.GuestCredentials; +import javax.jcr.RepositoryException; +import javax.jcr.SimpleCredentials; +import javax.security.auth.Subject; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.api.AuthInfo; +import org.apache.jackrabbit.oak.security.user.CredentialsImpl; +import org.apache.jackrabbit.oak.spi.security.authentication.Authentication; +import org.apache.jackrabbit.oak.spi.security.authentication.ImpersonationCredentials; +import org.apache.jackrabbit.oak.spi.security.user.util.PasswordUtility; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of the Authentication interface that validates credentials + * against user information stored in the repository. If no user exists with + * the specified userID or if the user has been disabled authentication will + * will fail irrespective of the specified credentials. Otherwise the following + * validation is performed: + * + *

    + *
  • {@link SimpleCredentials}: Authentication succeeds if userID and + * password match the information exposed by the {@link UserManager}.
  • + *
  • {@link ImpersonationCredentials}: Authentication succeeds if the + * subject to be authenticated is allowed to impersonate the user identified + * by the userID.
  • + *
  • {@link GuestCredentials}: The authentication succeeds if an 'anonymous' + * user exists in the repository.
  • + *
+ * + * For any other credentials {@link #authenticate(javax.jcr.Credentials)} + * will return {@code false} indicating that this implementation is not able + * to verify their validity. + */ +class UserAuthentication implements Authentication { + + private static final Logger log = LoggerFactory.getLogger(UserAuthentication.class); + + private final String userId; + private final UserManager userManager; + + UserAuthentication(String userId, UserManager userManager) { + this.userId = userId; + this.userManager = userManager; + } + + @Override + public boolean authenticate(Credentials credentials) throws LoginException { + if (userId == null || userManager == null) { + return false; + } + + boolean success = false; + try { + Authorizable authorizable = userManager.getAuthorizable(userId); + if (authorizable == null || authorizable.isGroup()) { + throw new LoginException("Unknown user " + userId); + } + + User user = (User) authorizable; + if (user.isDisabled()) { + throw new LoginException("User with ID " + userId + " has been disabled: "+ user.getDisabledReason()); + } + + if (credentials instanceof SimpleCredentials) { + SimpleCredentials creds = (SimpleCredentials) credentials; + Credentials userCreds = user.getCredentials(); + if (userId.equals(creds.getUserID()) && userCreds instanceof CredentialsImpl) { + success = PasswordUtility.isSame(((CredentialsImpl) userCreds).getPasswordHash(), creds.getPassword()); + } + checkSuccess(success, "UserId/Password mismatch."); + } else if (credentials instanceof ImpersonationCredentials) { + ImpersonationCredentials ipCreds = (ImpersonationCredentials) credentials; + AuthInfo info = ipCreds.getImpersonatorInfo(); + success = equalUserId(ipCreds) && impersonate(info, user); + checkSuccess(success, "Impersonation not allowed."); + } else { + // guest login is allowed if an anonymous user exists in the content (see get user above) + success = (credentials instanceof GuestCredentials); + } + } catch (RepositoryException e) { + throw new LoginException(e.getMessage()); + } + return success; + } + + //-------------------------------------------------------------------------- + private static void checkSuccess(boolean success, String msg) throws LoginException { + if (!success) { + throw new LoginException(msg); + } + } + + private boolean equalUserId(ImpersonationCredentials creds) { + Credentials base = creds.getBaseCredentials(); + return (base instanceof SimpleCredentials) && userId.equals(((SimpleCredentials) base).getUserID()); + } + + private boolean impersonate(AuthInfo info, User user) { + try { + if (info.getUserID().equals(user.getID())) { + log.debug("User " + info.getUserID() + " wants to impersonate himself -> success."); + return true; + } else { + log.debug("User " + info.getUserID() + " wants to impersonate " + user.getID()); + Subject subject = new Subject(true, info.getPrincipals(), Collections.emptySet(), Collections.emptySet()); + return user.getImpersonation().allows(subject); + } + } catch (RepositoryException e) { + log.debug("Error while validating impersonation", e.getMessage()); + } + return false; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/AccessControlContextImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/AccessControlContextImpl.java new file mode 100644 index 00000000000..4f730d087d1 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/AccessControlContextImpl.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authorization; + +import java.security.Principal; +import java.util.Set; +import javax.security.auth.Subject; + +import org.apache.jackrabbit.oak.spi.security.authorization.AccessControlContext; +import org.apache.jackrabbit.oak.spi.security.authorization.AllPermissions; +import org.apache.jackrabbit.oak.spi.security.authorization.CompiledPermissions; +import org.apache.jackrabbit.oak.spi.security.principal.AdminPrincipal; +import org.apache.jackrabbit.oak.spi.security.principal.SystemPrincipal; + +/** + * PermissionProviderImpl... TODO + */ +class AccessControlContextImpl implements AccessControlContext { + + private final Subject subject; + + AccessControlContextImpl(Subject subject) { + this.subject = subject; + } + + //-----------------------------------------------< AccessControlContext >--- + + @Override + public CompiledPermissions getPermissions() { + Set principals = subject.getPrincipals(); + if (principals.contains(SystemPrincipal.INSTANCE) || isAdmin(principals)) { + return AllPermissions.getInstance(); + } else { + // TODO: replace with permissions based on ac evaluation + return new CompiledPermissionImpl(principals); + } + } + + //-------------------------------------------------------------------------- + private static boolean isAdmin(Set principals) { + for (Principal principal : principals) { + if (principal instanceof AdminPrincipal) { + return true; + } + } + return false; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/AccessControlObserver.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/AccessControlObserver.java new file mode 100644 index 00000000000..fefbdcd7581 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/AccessControlObserver.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.security.authorization; + +import org.apache.jackrabbit.oak.spi.commit.Observer; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * {@code Observer} implementation that processes any modification made to + * access control content and updates persisted permission caches associated + * with access control related data stored in the repository. + */ +public class AccessControlObserver implements Observer { + + @Override + public void contentChanged(NodeState before, NodeState after) { + // TODO + throw new UnsupportedOperationException("not yet implemented"); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/AccessControlProviderImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/AccessControlProviderImpl.java new file mode 100644 index 00000000000..44454ecef36 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/AccessControlProviderImpl.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authorization; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.security.auth.Subject; + +import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; +import org.apache.jackrabbit.oak.spi.security.SecurityConfiguration; +import org.apache.jackrabbit.oak.spi.security.authorization.AccessControlContext; +import org.apache.jackrabbit.oak.spi.security.authorization.AccessControlProvider; + +/** + * {@code AccessControlProviderImpl} is a default implementation and + * creates {@link AccessControlContextImpl} for a given set of principals. + */ +public class AccessControlProviderImpl extends SecurityConfiguration.Default implements AccessControlProvider { + + @Override + public AccessControlContext getAccessControlContext(Subject subject) { + return new AccessControlContextImpl(subject); + } + + @Override + public List getValidatorProviders() { + List vps = new ArrayList(); + vps.add(new PermissionValidatorProvider()); + vps.add(new AccessControlValidatorProvider()); + return Collections.unmodifiableList(vps); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/AccessControlValidator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/AccessControlValidator.java new file mode 100644 index 00000000000..b0914b7a4c9 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/AccessControlValidator.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authorization; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * AccessControlValidator... TODO + */ +class AccessControlValidator implements Validator { + + //----------------------------------------------------------< Validator >--- + @Override + public void propertyAdded(PropertyState after) throws CommitFailedException { + // TODO: validate access control property + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) throws CommitFailedException { + // TODO: validate access control property + } + + @Override + public void propertyDeleted(PropertyState before) throws CommitFailedException { + // nothing to do: mandatory properties will be enforced by node type validator + } + + @Override + public Validator childNodeAdded(String name, NodeState after) throws CommitFailedException { + // TODO validate new acl / ace + return null; + } + + @Override + public Validator childNodeChanged(String name, NodeState before, NodeState after) throws CommitFailedException { + // TODO validate acl / ace / restriction modification + return null; + } + + @Override + public Validator childNodeDeleted(String name, NodeState before) throws CommitFailedException { + // TODO validate acl / ace / restriction removal + return null; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/AccessControlValidatorProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/AccessControlValidatorProvider.java new file mode 100644 index 00000000000..ce199319dcd --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/AccessControlValidatorProvider.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authorization; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * {@code AccessControlValidatorProvider} aimed to provide a root validator + * that makes sure access control related content modifications (adding, modifying + * and removing access control policies) are valid according to the + * constraints defined by this access control implementation. + */ +class AccessControlValidatorProvider implements ValidatorProvider { + + @Nonnull + @Override + public Validator getRootValidator(NodeState before, NodeState after) { + return new AccessControlValidator(); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/CompiledPermissionImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/CompiledPermissionImpl.java new file mode 100644 index 00000000000..c99d5ad0b04 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/CompiledPermissionImpl.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authorization; + +import java.security.Principal; +import java.util.Set; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.spi.security.authorization.CompiledPermissions; +import org.apache.jackrabbit.oak.spi.security.authorization.Permissions; + +/** + * TODO + */ +class CompiledPermissionImpl implements CompiledPermissions { + + CompiledPermissionImpl(Set principals) { + + } + + @Override + public boolean canRead(Tree tree) { + // TODO + return true; + } + + @Override + public boolean canRead(Tree tree, PropertyState property) { + // TODO + return true; + } + + @Override + public boolean isGranted(int permissions) { + // TODO + return false; + } + + @Override + public boolean isGranted(Tree tree, int permissions) { + // TODO + return (permissions == Permissions.READ_NODE); + } + + @Override + public boolean isGranted(Tree parent, PropertyState property, int permissions) { + // TODO + return (permissions == Permissions.READ_PROPERTY); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/PermissionValidator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/PermissionValidator.java new file mode 100644 index 00000000000..244ea0abb36 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/PermissionValidator.java @@ -0,0 +1,228 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authorization; + +import javax.jcr.AccessDeniedException; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.plugins.name.NamespaceConstants; +import org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants; +import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.security.authorization.CompiledPermissions; +import org.apache.jackrabbit.oak.spi.security.authorization.Permissions; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.util.NodeUtil; +import org.apache.jackrabbit.oak.version.VersionConstants; +import org.apache.jackrabbit.util.Text; + +/** + * PermissionValidator... TODO + */ +class PermissionValidator implements Validator { + + /* TODO + * - special permissions for protected items (versioning, access control, etc.) + * - Renaming nodes or Move with same parent are reflected as remove+add -> needs special handling + * - review usage of OAK_CHILD_ORDER property (in particular if the property was removed + */ + + private final CompiledPermissions compiledPermissions; + + private final NodeUtil parentBefore; + private final NodeUtil parentAfter; + + PermissionValidator(CompiledPermissions compiledPermissions, + NodeUtil parentBefore, NodeUtil parentAfter) { + this.compiledPermissions = compiledPermissions; + this.parentBefore = parentBefore; + this.parentAfter = parentAfter; + + } + + //----------------------------------------------------------< Validator >--- + @Override + public void propertyAdded(PropertyState after) throws CommitFailedException { + checkPermissions(parentAfter, after, Permissions.ADD_PROPERTY); + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) throws CommitFailedException { + checkPermissions(parentAfter, after, Permissions.MODIFY_PROPERTY); + } + + @Override + public void propertyDeleted(PropertyState before) throws CommitFailedException { + checkPermissions(parentBefore, before, Permissions.REMOVE_PROPERTY); + } + + @Override + public Validator childNodeAdded(String name, NodeState after) throws CommitFailedException { + NodeUtil child = parentAfter.getChild(name); + return checkPermissions(child, false, Permissions.ADD_NODE); + } + + @Override + public Validator childNodeChanged(String name, NodeState before, NodeState after) throws CommitFailedException { + NodeUtil childBefore = parentBefore.getChild(name); + NodeUtil childAfter = parentAfter.getChild(name); + + // TODO + + return new PermissionValidator(compiledPermissions, childBefore, childAfter); + } + + @Override + public Validator childNodeDeleted(String name, NodeState before) throws CommitFailedException { + NodeUtil child = parentBefore.getChild(name); + return checkPermissions(child, true, Permissions.REMOVE_NODE); + } + + //------------------------------------------------------------< private >--- + private void checkPermissions(NodeUtil parent, PropertyState property, int defaultPermission) throws CommitFailedException { + String parentPath = parent.getTree().getPath(); + String name = property.getName(); + + int permission; + if (JcrConstants.JCR_PRIMARYTYPE.equals(name) || JcrConstants.JCR_MIXINTYPES.equals(name)) { + // TODO: distinguish between autocreated and user-supplied modification (?) + permission = Permissions.NODE_TYPE_MANAGEMENT; + } else if (isLockProperty(name)) { + permission = Permissions.LOCK_MANAGEMENT; + } else if (isNamespaceDefinition(parentPath)) { + permission = Permissions.NAMESPACE_MANAGEMENT; + } else if (isNodeTypeDefinition(parentPath)) { + permission = Permissions.NODE_TYPE_DEFINITION_MANAGEMENT; + } else if (isPrivilegeDefinition(parentPath)) { + permission = Permissions.PRIVILEGE_MANAGEMENT; + } else if (isAccessControl(parent)) { + permission = Permissions.MODIFY_ACCESS_CONTROL; + } else if (isVersionProperty(parent, property)) { + permission = Permissions.VERSION_MANAGEMENT; + // FIXME: path to check for permission must be adjusted to be + // the one of the versionable node instead of the target parent. + } else { + // TODO: identify specific permission depending on type of protection + // - user/group property -> user management + permission = defaultPermission; + } + + checkPermissions(parent.getTree(), property, permission); + } + + private PermissionValidator checkPermissions(NodeUtil node, boolean isBefore, int defaultPermission) throws CommitFailedException { + String path = node.getTree().getPath(); + int permission; + + if (isNamespaceDefinition(path)) { + permission = Permissions.NAMESPACE_MANAGEMENT; + } else if (isNodeTypeDefinition(path)) { + permission = Permissions.NODE_TYPE_DEFINITION_MANAGEMENT; + } else if (isPrivilegeDefinition(path)) { + permission = Permissions.PRIVILEGE_MANAGEMENT; + } else if (isAccessControl(node)) { + permission = Permissions.MODIFY_ACCESS_CONTROL; + } else if (isVersion(node)) { + permission = Permissions.VERSION_MANAGEMENT; + // FIXME: path to check for permission must be adjusted to be + // // the one of the versionable node instead of the target node. + } else { + // TODO: identify specific permission depending on additional types of protection + // - user/group -> user management + // - workspace management ??? + // TODO: identify renaming/move of nodes that only required MODIFY_CHILD_NODE_COLLECTION permission + permission = defaultPermission; + } + + if (Permissions.isRepositoryPermission(permission)) { + checkPermissions(permission); + return null; // no need for further validation down the subtree + } else { + checkPermissions(node.getTree(), permission); + return (isBefore) ? + new PermissionValidator(compiledPermissions, node, null) : + new PermissionValidator(compiledPermissions, null, node); + } + } + + private void checkPermissions(int permissions) throws CommitFailedException { + if (!compiledPermissions.isGranted(permissions)) { + throw new CommitFailedException(new AccessDeniedException()); + } + } + + private void checkPermissions(Tree tree, int permissions) throws CommitFailedException { + if (!compiledPermissions.isGranted(tree, permissions)) { + throw new CommitFailedException(new AccessDeniedException()); + } + } + + private void checkPermissions(Tree parent, PropertyState property, int permissions) throws CommitFailedException { + if (!compiledPermissions.isGranted(parent, property, permissions)) { + throw new CommitFailedException(new AccessDeniedException()); + } + } + + private static boolean isAccessControl(NodeUtil node) { + // TODO: depends on ac-model + return false; + } + + private static boolean isVersion(NodeUtil node) { + if (node.getTree().isRoot()) { + return false; + } + // TODO: review again + if (VersionConstants.VERSION_NODE_NAMES.contains(node.getName())) { + return true; + } else if (VersionConstants.VERSION_NODE_TYPE_NAMES.contains(node.getName(JcrConstants.JCR_PRIMARYTYPE))) { + return true; + } else { + String path = node.getTree().getPath(); + return VersionConstants.SYSTEM_PATHS.contains(Text.getAbsoluteParent(path, 1)); + } + } + + private static boolean isVersionProperty(NodeUtil parent, PropertyState property) { + if (VersionConstants.VERSION_PROPERTY_NAMES.contains(property.getName())) { + return true; + } else { + return isVersion(parent); + } + } + + private static boolean isLockProperty(String name) { + return JcrConstants.JCR_LOCKISDEEP.equals(name) || JcrConstants.JCR_LOCKOWNER.equals(name); + } + + private static boolean isNamespaceDefinition(String path) { + // TODO: depends on pluggable module + return Text.isDescendant(NamespaceConstants.NAMESPACES_PATH, path); + } + private static boolean isNodeTypeDefinition(String path) { + // TODO: depends on pluggable module + return Text.isDescendant(NodeTypeConstants.NODE_TYPES_PATH, path); + } + + private static boolean isPrivilegeDefinition(String path) { + // TODO: depends on pluggable module + return Text.isDescendant(PrivilegeConstants.PRIVILEGES_PATH, path); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/PermissionValidatorProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/PermissionValidatorProvider.java new file mode 100644 index 00000000000..935b2949b66 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/PermissionValidatorProvider.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.security.authorization; + +import java.security.AccessController; + +import javax.annotation.Nonnull; +import javax.security.auth.Subject; + +import org.apache.jackrabbit.oak.core.ReadOnlyTree; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; +import org.apache.jackrabbit.oak.spi.security.authorization.AccessControlContext; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.util.NodeUtil; + +/** + * PermissionValidatorProvider... TODO + */ +public class PermissionValidatorProvider implements ValidatorProvider { + + @Nonnull + @Override + public Validator getRootValidator(NodeState before, NodeState after) { + Subject subject = Subject.getSubject(AccessController.getContext()); + if (subject == null) { + // use empty subject + subject = new Subject(); + } + + // FIXME: should use same provider as in ContentRepositoryImpl + AccessControlContext context = new AccessControlProviderImpl() + .getAccessControlContext(subject); + + NodeUtil rootBefore = new NodeUtil(new ReadOnlyTree(before)); + NodeUtil rootAfter = new NodeUtil(new ReadOnlyTree(after)); + return new PermissionValidator(context.getPermissions(), rootBefore, rootAfter); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/principal/AdminPrincipalImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/principal/AdminPrincipalImpl.java new file mode 100644 index 00000000000..7450ffad693 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/principal/AdminPrincipalImpl.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.security.principal; + +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.namepath.PathMapper; +import org.apache.jackrabbit.oak.spi.security.principal.AdminPrincipal; +import org.apache.jackrabbit.oak.spi.security.principal.TreeBasedPrincipal; + +/** + * AdminPrincipal variant of the {@link TreeBasedPrincipal}. + */ +public class AdminPrincipalImpl extends TreeBasedPrincipal implements AdminPrincipal { + + public AdminPrincipalImpl(String principalName, Tree tree, PathMapper pathMapper) { + super(principalName, tree, pathMapper); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/principal/PrincipalManagerImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/principal/PrincipalManagerImpl.java new file mode 100644 index 00000000000..efad1289105 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/principal/PrincipalManagerImpl.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.security.principal; + +import java.security.Principal; + +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.oak.spi.security.principal.PrincipalIteratorAdapter; +import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.oak.spi.security.principal.PrincipalProvider; + +/** + * PrincipalManagerImpl... + */ +public class PrincipalManagerImpl implements PrincipalManager { + + private final PrincipalProvider principalProvider; + + public PrincipalManagerImpl(PrincipalProvider principalProvider) { + this.principalProvider = principalProvider; + } + + //---------------------------------------------------< PrincipalManager >--- + @Override + public boolean hasPrincipal(String principalName) { + return principalProvider.getPrincipal(principalName) != null; + } + + @Override + public Principal getPrincipal(String principalName) { + return principalProvider.getPrincipal(principalName); + } + + @Override + public PrincipalIterator findPrincipals(String simpleFilter) { + return findPrincipals(simpleFilter, PrincipalManager.SEARCH_TYPE_ALL); + } + + @Override + public PrincipalIterator findPrincipals(String simpleFilter, int searchType) { + return new PrincipalIteratorAdapter(principalProvider.findPrincipals(simpleFilter, searchType)); + } + + @Override + public PrincipalIterator getPrincipals(int searchType) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public PrincipalIterator getGroupMembership(Principal principal) { + return new PrincipalIteratorAdapter(principalProvider.getGroupMembership(principal)); + } + + @Override + public Principal getEveryone() { + Principal everyone = getPrincipal(EveryonePrincipal.NAME); + if (everyone == null) { + everyone = EveryonePrincipal.getInstance(); + } + return everyone; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/principal/PrincipalProviderImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/principal/PrincipalProviderImpl.java new file mode 100644 index 00000000000..5e050566b26 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/principal/PrincipalProviderImpl.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.principal; + +import java.security.Principal; +import java.security.acl.Group; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import javax.jcr.RepositoryException; + +import com.google.common.base.Function; +import com.google.common.base.Predicates; +import com.google.common.collect.Iterators; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.oak.spi.security.principal.PrincipalProvider; +import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@code PrincipalProviderImpl} is a principal provider implementation + * that operates on principal information read from user information exposed by + * the configured {@link UserManager}. + */ +public class PrincipalProviderImpl implements PrincipalProvider { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(PrincipalProviderImpl.class); + + private final UserManager userManager; + + public PrincipalProviderImpl(Root root, + UserConfiguration userConfiguration, + NamePathMapper namePathMapper) { + this.userManager = userConfiguration.getUserManager(root, namePathMapper); + } + + //--------------------------------------------------< PrincipalProvider >--- + @Override + public Principal getPrincipal(final String principalName) { + Authorizable authorizable = getAuthorizable(new Principal() { + @Override + public String getName() { + return principalName; + } + }); + if (authorizable != null) { + try { + return authorizable.getPrincipal(); + } catch (RepositoryException e) { + log.debug(e.getMessage()); + } + } + + // no such principal or error while accessing principal from user/group + return (EveryonePrincipal.NAME.equals(principalName)) ? EveryonePrincipal.getInstance() : null; + } + + @Override + public Set getGroupMembership(Principal principal) { + Authorizable authorizable = getAuthorizable(principal); + if (authorizable == null) { + return Collections.emptySet(); + } else { + return getGroupMembership(authorizable); + } + } + + @Override + public Set getPrincipals(String userID) { + Set principals = new HashSet(); + try { + Authorizable authorizable = userManager.getAuthorizable(userID); + if (authorizable != null && !authorizable.isGroup()) { + principals.add(authorizable.getPrincipal()); + principals.addAll(getGroupMembership(authorizable)); + } + } catch (RepositoryException e) { + log.debug(e.getMessage()); + } + return principals; + } + + @Override + public Iterator findPrincipals(String nameHint, int searchType) { + try { + Iterator authorizables = userManager.findAuthorizables(UserConstants.REP_PRINCIPAL_NAME, nameHint, UserManager.SEARCH_TYPE_AUTHORIZABLE); + return Iterators.transform( + Iterators.filter(authorizables, Predicates.notNull()), + new AuthorizableToPrincipal()); + } catch (RepositoryException e) { + log.debug(e.getMessage()); + return Iterators.emptyIterator(); + } + } + + //------------------------------------------------------------< private >--- + private Authorizable getAuthorizable(Principal principal) { + try { + return userManager.getAuthorizable(principal); + } catch (RepositoryException e) { + log.debug("Error while retrieving principal: ", e.getMessage()); + return null; + } + } + + private Set getGroupMembership(Authorizable authorizable) { + Set groupPrincipals = new HashSet(); + try { + Iterator groups = authorizable.memberOf(); + while (groups.hasNext()) { + Principal grPrincipal = groups.next().getPrincipal(); + if (grPrincipal instanceof Group) { + groupPrincipals.add((Group) grPrincipal); + } + } + } catch (RepositoryException e) { + log.debug(e.getMessage()); + } + groupPrincipals.add(EveryonePrincipal.getInstance()); + return groupPrincipals; + } + + //-------------------------------------------------------------------------- + + /** + * Function to covert an authorizable tree to a principal. + */ + private final class AuthorizableToPrincipal implements Function { + @Override + public Principal apply(Authorizable authorizable) { + try { + return authorizable.getPrincipal(); + } catch (RepositoryException e) { + log.debug(e.getMessage()); + return null; + } + } + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeConfigurationImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeConfigurationImpl.java new file mode 100644 index 00000000000..a67e0289c3b --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeConfigurationImpl.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.security.privilege; + +import java.util.Collections; +import java.util.List; +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; +import org.apache.jackrabbit.oak.spi.security.SecurityConfiguration; +import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConfiguration; +import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeManagerImpl; +import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeProvider; + +/** + * PrivilegeConfigurationImpl... TODO + */ +public class PrivilegeConfigurationImpl extends SecurityConfiguration.Default implements PrivilegeConfiguration { + + @Override + public PrivilegeProvider getPrivilegeProvider(ContentSession contentSession, Root root) { + return new PrivilegeRegistry(contentSession, root); + } + + @Nonnull + @Override + public PrivilegeManager getPrivilegeManager(ContentSession contentSession, Root root, NamePathMapper namePathMapper) { + return new PrivilegeManagerImpl(root, getPrivilegeProvider(contentSession, root), namePathMapper); + } + + @Override + public List getValidatorProviders() { + ValidatorProvider vp = new PrivilegeValidatorProvider(); + return Collections.singletonList(vp); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeDefinitionImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeDefinitionImpl.java new file mode 100644 index 00000000000..836dda7c245 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeDefinitionImpl.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.privilege; + +import java.util.Collections; +import java.util.Set; +import javax.annotation.Nonnull; + +import com.google.common.collect.ImmutableSet; +import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeDefinition; + +/** + * PrivilegeDefinitionImpl... TODO + */ +class PrivilegeDefinitionImpl implements PrivilegeDefinition { + + private final String name; + private final boolean isAbstract; + private final Set declaredAggregateNames; + + PrivilegeDefinitionImpl(String name, boolean isAbstract, + Set declaredAggregateNames) { + this.name = name; + this.isAbstract = isAbstract; + this.declaredAggregateNames = declaredAggregateNames; + } + + PrivilegeDefinitionImpl(String name, boolean isAbstract, + String... declaredAggregateNames) { + this(name, isAbstract, (declaredAggregateNames == null) ? + Collections.emptySet() : + ImmutableSet.copyOf(declaredAggregateNames)); + } + + //------------------------------------------------< PrivilegeDefinition >--- + @Nonnull + @Override + public String getName() { + return name; + } + + @Override + public boolean isAbstract() { + return isAbstract; + } + + @Nonnull + @Override + public Set getDeclaredAggregateNames() { + return declaredAggregateNames; + } + + //-------------------------------------------------------------< Object >--- + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + (isAbstract ? 1 : 0); + result = 31 * result + declaredAggregateNames.hashCode(); + return result; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o instanceof PrivilegeDefinitionImpl) { + PrivilegeDefinitionImpl other = (PrivilegeDefinitionImpl) o; + return name.equals(other.name) && + isAbstract == other.isAbstract && + declaredAggregateNames.equals(other.declaredAggregateNames); + } else { + return false; + } + } + + @Override + public String toString() { + return "PrivilegeDefinition: " + name; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeDefinitionReader.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeDefinitionReader.java new file mode 100644 index 00000000000..fb5dc81aa17 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeDefinitionReader.java @@ -0,0 +1,262 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.privilege; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeDefinition; +import org.apache.jackrabbit.oak.util.NodeUtil; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import static org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants.PRIVILEGES_PATH; +import static org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants.REP_AGGREGATES; +import static org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants.REP_IS_ABSTRACT; + + +/** + * Reads privilege definitions without applying any validation. + */ +class PrivilegeDefinitionReader { + + private final Tree privilegesTree; + + PrivilegeDefinitionReader(Tree privilegesTree) { + this.privilegesTree = privilegesTree; + } + + PrivilegeDefinitionReader(Root root) { + this(root.getTree(PRIVILEGES_PATH)); + } + + Map readDefinitions() { + Map definitions = new HashMap(); + if (privilegesTree != null) { + for (Tree child : privilegesTree.getChildren()) { + PrivilegeDefinition def = readDefinition(child); + definitions.put(def.getName(), def); + } + } + return definitions; + } + + PrivilegeDefinition readDefinition(Tree definitionTree) { + NodeUtil n = new NodeUtil(definitionTree); + String name = n.getName(); + boolean isAbstract = n.getBoolean(REP_IS_ABSTRACT); + String[] declAggrNames = n.getStrings(REP_AGGREGATES); + + return new PrivilegeDefinitionImpl(name, isAbstract, declAggrNames); + } + + /** + * Reads privilege definitions for the specified {@code InputStream}. The + * aim of this method is to provide backwards compatibility with + * custom privilege definitions of Jackrabbit 2.x repositories. The caller + * is in charge of migrating the definitions. + * + * @param customPrivileges + * @param nsRegistry + * @return + * @throws RepositoryException + * @throws IOException + */ + static PrivilegeDefinition[] readCustomDefinitons(InputStream customPrivileges, + NamespaceRegistry nsRegistry) throws RepositoryException, IOException { + Map definitions = new LinkedHashMap(); + InputSource src = new InputSource(customPrivileges); + for (PrivilegeDefinition def : PrivilegeXmlHandler.readDefinitions(src, nsRegistry)) { + String privName = def.getName(); + if (definitions.containsKey(privName)) { + throw new RepositoryException("Duplicate entry for custom privilege with name " + privName.toString()); + } + definitions.put(privName, def); + } + return definitions.values().toArray(new PrivilegeDefinition[definitions.size()]); + } + + + + //-------------------------------------------------------------------------- + /** + * The {@code PrivilegeXmlHandler} loads privilege definitions from a XML + * document using the following format: + *
+     *  <!DOCTYPE privileges [
+     *  <!ELEMENT privileges (privilege)+>
+     *  <!ELEMENT privilege (contains)+>
+     *  <!ATTLIST privilege abstract (true|false) false>
+     *  <!ATTLIST privilege name NMTOKEN #REQUIRED>
+     *  <!ELEMENT contains EMPTY>
+     *  <!ATTLIST contains name NMTOKEN #REQUIRED>
+     * ]>
+     * 
+ */ + private static class PrivilegeXmlHandler { + + private static final String TEXT_XML = "text/xml"; + private static final String APPLICATION_XML = "application/xml"; + + private static final String XML_PRIVILEGES = "privileges"; + private static final String XML_PRIVILEGE = "privilege"; + private static final String XML_CONTAINS = "contains"; + + private static final String ATTR_NAME = "name"; + private static final String ATTR_ABSTRACT = "abstract"; + + private static final String ATTR_XMLNS = "xmlns:"; + + private static DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = createFactory(); + + private static DocumentBuilderFactory createFactory() { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setIgnoringComments(false); + factory.setIgnoringElementContentWhitespace(true); + return factory; + } + + private static PrivilegeDefinition[] readDefinitions(InputSource input, + NamespaceRegistry nsRegistry) throws RepositoryException, IOException { + try { + List defs = new ArrayList(); + + DocumentBuilder builder = createDocumentBuilder(); + Document doc = builder.parse(input); + Element root = doc.getDocumentElement(); + if (!XML_PRIVILEGES.equals(root.getNodeName())) { + throw new IllegalArgumentException("root element must be named 'privileges'"); + } + + updateNamespaceMapping(root, nsRegistry); + + NodeList nl = root.getElementsByTagName(XML_PRIVILEGE); + for (int i = 0; i < nl.getLength(); i++) { + Node n = nl.item(i); + PrivilegeDefinition def = parseDefinition(n, nsRegistry); + if (def != null) { + defs.add(def); + } + } + return defs.toArray(new PrivilegeDefinition[defs.size()]); + + } catch (SAXException e) { + throw new RepositoryException(e); + } catch (ParserConfigurationException e) { + throw new RepositoryException(e); + } + } + + /** + * Build a new {@code PrivilegeDefinition} from the given XML node. + * @param n the xml node storing the privilege definition. + * @param nsRegistry + * @return a new PrivilegeDefinition. + * @throws javax.jcr.RepositoryException + */ + private static PrivilegeDefinition parseDefinition(Node n, NamespaceRegistry nsRegistry) throws RepositoryException { + if (n.getNodeType() == Node.ELEMENT_NODE) { + Element elem = (Element) n; + + updateNamespaceMapping(elem, nsRegistry); + + String name = elem.getAttribute(ATTR_NAME); + boolean isAbstract = Boolean.parseBoolean(elem.getAttribute(ATTR_ABSTRACT)); + + Set aggrNames = new HashSet(); + NodeList nodeList = elem.getChildNodes(); + for (int i = 0; i < nodeList.getLength(); i++) { + Node contains = nodeList.item(i); + if (isElement(n) && XML_CONTAINS.equals(contains.getNodeName())) { + String aggrName = ((Element) contains).getAttribute(ATTR_NAME); + if (aggrName != null) { + aggrNames.add(aggrName); + } + } + } + return new PrivilegeDefinitionImpl(name, isAbstract, aggrNames); + } + + // could not parse into privilege definition + return null; + } + + /** + * Create a new {@code DocumentBuilder} + * + * @return a new {@code DocumentBuilder} + * @throws ParserConfigurationException + */ + private static DocumentBuilder createDocumentBuilder() throws ParserConfigurationException { + DocumentBuilder builder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder(); + builder.setErrorHandler(new DefaultHandler()); + return builder; + } + + /** + * Update the specified nsRegistry mappings with the nsRegistry declarations + * defined by the given XML element. + * + * @param elem + * @param nsRegistry + * @throws javax.jcr.RepositoryException + */ + private static void updateNamespaceMapping(Element elem, NamespaceRegistry nsRegistry) throws RepositoryException { + NamedNodeMap attributes = elem.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + Attr attr = (Attr) attributes.item(i); + if (attr.getName().startsWith(ATTR_XMLNS)) { + String prefix = attr.getName().substring(ATTR_XMLNS.length()); + String uri = attr.getValue(); + nsRegistry.registerNamespace(prefix, uri); + } + } + } + + /** + * Returns {@code true} if the given XML node is an element. + * + * @param n + * @return {@code true} if the given XML node is an element; {@code false} otherwise. + */ + private static boolean isElement(Node n) { + return n.getNodeType() == Node.ELEMENT_NODE; + } + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeMigrator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeMigrator.java new file mode 100644 index 00000000000..65c1a0b5304 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeMigrator.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.security.privilege; + +import java.io.IOException; +import java.io.InputStream; +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeDefinition; +import org.apache.jackrabbit.oak.util.TODO; + +/** + * PrivilegeMigrator is a utility to migrate custom privilege definitions from + * a jackrabbit 2 project to oak. + */ +public class PrivilegeMigrator { + + private final ContentSession contentSession; + + public PrivilegeMigrator(ContentSession contentSession) { + this.contentSession = contentSession; + } + + /** + * + * @throws RepositoryException + */ + public void migrateCustomPrivileges() throws RepositoryException { + PrivilegeRegistry pr = new PrivilegeRegistry(contentSession, contentSession.getLatestRoot()); + InputStream stream = null; + // TODO: order custom privileges such that validation succeeds. + // FIXME: user proper path to jr2 custom privileges stored in fs + // jr2 used to be: + // new FileSystemResource(fs, "/privileges/custom_privileges.xml").getInputStream() + if (stream != null) { + try { + // TODO: should get a proper namespace registry from somewhere + NamespaceRegistry nsRegistry = + TODO.dummyImplementation().returnValue(null); + PrivilegeDefinition[] custom = PrivilegeDefinitionReader.readCustomDefinitons(stream, nsRegistry); + + for (PrivilegeDefinition def : custom) { + pr.registerDefinition(def.getName(), def.isAbstract(), def.getDeclaredAggregateNames()); + } + } catch (IOException e) { + throw new RepositoryException(e); + } finally { + try { + stream.close(); + } catch (IOException e) { + // ignore. + } + } + } + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeRegistry.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeRegistry.java new file mode 100644 index 00000000000..6c4b71fc17a --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeRegistry.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.privilege; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nonnull; +import javax.jcr.RepositoryException; + +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.spi.security.privilege.PrivilegeConstants; +import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeDefinition; +import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeProvider; +import org.apache.jackrabbit.oak.util.NodeUtil; + +/** + * PrivilegeRegistry... TODO + * + * + * TODO: define if/how built-in privileges are reflected in the mk + * TODO: define if custom privileges are read with editing content session (thus enforcing read permissions) + */ +public class PrivilegeRegistry implements PrivilegeProvider, PrivilegeConstants { + + private static final Map AGGREGATE_PRIVILEGES = new HashMap(); + static { + AGGREGATE_PRIVILEGES.put(JCR_READ, AGGR_JCR_READ); + AGGREGATE_PRIVILEGES.put(JCR_MODIFY_PROPERTIES, AGGR_JCR_MODIFY_PROPERTIES); + AGGREGATE_PRIVILEGES.put(JCR_WRITE, AGGR_JCR_WRITE); + AGGREGATE_PRIVILEGES.put(REP_WRITE, AGGR_REP_WRITE); + } + + private final ContentSession contentSession; + private final Root root; + + private final Map definitions; + + public PrivilegeRegistry(ContentSession contentSession, Root root) { + this.contentSession = contentSession; + this.root = root; + this.definitions = getAllDefinitions(new PrivilegeDefinitionReader(root)); + } + + static Map getAllDefinitions(PrivilegeDefinitionReader reader) { + Map definitions = new HashMap(); + for (String privilegeName : NON_AGGR_PRIVILEGES) { + PrivilegeDefinition def = new PrivilegeDefinitionImpl(privilegeName, false); + definitions.put(privilegeName, def); + } + + for (String privilegeName : AGGREGATE_PRIVILEGES.keySet()) { + PrivilegeDefinition def = new PrivilegeDefinitionImpl(privilegeName, false, AGGREGATE_PRIVILEGES.get(privilegeName)); + definitions.put(privilegeName, def); + } + + updateCustomDefinitions(reader, definitions); + updateJcrAllPrivilege(definitions); + + return definitions; + } + + private static void updateCustomDefinitions(PrivilegeDefinitionReader reader, Map definitions) { + definitions.putAll(reader.readDefinitions()); + } + + private static void updateJcrAllPrivilege(Map definitions) { + Map m = new HashMap(definitions); + m.remove(JCR_ALL); + definitions.put(JCR_ALL, new PrivilegeDefinitionImpl(JCR_ALL, false, m.keySet())); + } + + //--------------------------------------------------< PrivilegeProvider >--- + @Override + public void refresh() { + // re-read the definitions (TODO: evaluate if it was better to always read privileges on demand only.) + updateCustomDefinitions(new PrivilegeDefinitionReader(root), definitions); + updateJcrAllPrivilege(definitions); + } + + @Nonnull + @Override + public PrivilegeDefinition[] getPrivilegeDefinitions() { + return definitions.values().toArray(new PrivilegeDefinition[definitions.size()]); + } + + @Override + public PrivilegeDefinition getPrivilegeDefinition(String name) { + return definitions.get(name); + } + + @Override + public PrivilegeDefinition registerDefinition( + final String privilegeName, final boolean isAbstract, + final Set declaredAggregateNames) + throws RepositoryException { + + PrivilegeDefinition definition = new PrivilegeDefinitionImpl(privilegeName, isAbstract, declaredAggregateNames); + internalRegisterDefinitions(definition); + return definition; + } + + //------------------------------------------------------------< private >--- + + private void internalRegisterDefinitions(PrivilegeDefinition toRegister) throws RepositoryException { + Root latestRoot = contentSession.getLatestRoot(); + try { + // make sure the privileges path is defined + Tree privilegesTree = latestRoot.getTree(PRIVILEGES_PATH); + if (privilegesTree == null) { + throw new RepositoryException("Repository doesn't contain node " + PRIVILEGES_PATH); + } + + NodeUtil privilegesNode = new NodeUtil(privilegesTree); + writeDefinition(privilegesNode, toRegister); + + // delegate validation to the commit validation (see above) + latestRoot.commit(); + + } catch (CommitFailedException e) { + Throwable t = e.getCause(); + if (t instanceof RepositoryException) { + throw (RepositoryException) t; + } else { + throw new RepositoryException(e.getMessage()); + } + } + + root.refresh(); + definitions.put(toRegister.getName(), toRegister); + updateJcrAllPrivilege(definitions); + } + + private void writeDefinition(NodeUtil privilegesNode, PrivilegeDefinition definition) { + NodeUtil privNode = privilegesNode.addChild(definition.getName(), NT_REP_PRIVILEGE); + if (definition.isAbstract()) { + privNode.setBoolean(REP_IS_ABSTRACT, true); + } + Set declAggrNames = definition.getDeclaredAggregateNames(); + if (!declAggrNames.isEmpty()) { + String[] names = definition.getDeclaredAggregateNames().toArray(new String[declAggrNames.size()]); + privNode.setNames(REP_AGGREGATES, names); + } + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeValidator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeValidator.java new file mode 100644 index 00000000000..c068eba7bef --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeValidator.java @@ -0,0 +1,213 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.privilege; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.oak.api.CommitFailedException; +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.core.ReadOnlyTree; +import org.apache.jackrabbit.oak.plugins.name.NamespaceConstants; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants; +import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeDefinition; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.util.Text; + +/** + * Validator implementation that is responsible for validating any modifications + * made to privileges stored in the repository. + */ +class PrivilegeValidator implements PrivilegeConstants, Validator { + + private final Map definitions; + private final PrivilegeDefinitionReader reader; + + PrivilegeValidator(Tree rootBefore) { + Tree privilegesBefore = null; + Tree system = rootBefore.getChild(JcrConstants.JCR_SYSTEM); + if (system != null) { + privilegesBefore = system.getChild(REP_PRIVILEGES); + } + + if (privilegesBefore != null) { + reader = new PrivilegeDefinitionReader(privilegesBefore); + definitions = PrivilegeRegistry.getAllDefinitions(reader); + } else { + reader = null; + definitions = null; + } + } + + //----------------------------------------------------------< Validator >--- + @Override + public void propertyAdded(PropertyState after) throws CommitFailedException { + // no-op + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) throws CommitFailedException { + throw new CommitFailedException("Attempt to modify existing privilege definition."); + } + + @Override + public void propertyDeleted(PropertyState before) throws CommitFailedException { + throw new CommitFailedException("Attempt to modify existing privilege definition."); + } + + @Override + public Validator childNodeAdded(String name, NodeState after) throws CommitFailedException { + checkInitialized(); + + // the following characteristics are expected to be validated elsewhere: + // - permission to allow privilege registration -> permission validator. + // - name collisions (-> delegated to NodeTypeValidator since sms are not allowed) + // - name must be valid (-> delegated to NameValidator) + + // name may not contain reserved namespace prefix + if (NamespaceConstants.RESERVED_PREFIXES.contains(Text.getNamespacePrefix(name))) { + String msg = "Failed to register custom privilege: Definition uses reserved namespace: " + name; + throw new CommitFailedException(new RepositoryException(msg)); + } + + // primary node type name must be rep:privilege + Tree tree = new ReadOnlyTree(null, name, after); + PropertyState primaryType = tree.getProperty(JcrConstants.JCR_PRIMARYTYPE); + if (primaryType == null || !NT_REP_PRIVILEGE.equals(primaryType.getValue(Type.STRING))) { + throw new CommitFailedException("Privilege definition must have primary node type set to rep:privilege"); + } + + // additional validation of the definition + PrivilegeDefinition def = reader.readDefinition(tree); + validateDefinition(def); + + // privilege definitions may not have child nodes. + return null; + } + + @Override + public Validator childNodeChanged(String name, NodeState before, NodeState after) throws CommitFailedException { + throw new CommitFailedException("Attempt to modify existing privilege definition " + name); + } + + @Override + public Validator childNodeDeleted(String name, NodeState before) throws CommitFailedException { + throw new CommitFailedException("Attempt to un-register privilege " + name); + } + + //------------------------------------------------------------< private >--- + /** + * Validation of the privilege definition including the following steps: + * + * - all aggregates must have been registered before + * - no existing privilege defines the same aggregation + * - no cyclic aggregation + * + * @param definition The new privilege definition to validate. + * @throws org.apache.jackrabbit.oak.api.CommitFailedException If any of + * the checks listed above fails. + */ + private void validateDefinition(PrivilegeDefinition definition) throws CommitFailedException { + Set declaredNames = definition.getDeclaredAggregateNames(); + if (declaredNames.isEmpty()) { + return; + } + + if (declaredNames.size() == 1) { + throw new CommitFailedException("Singular aggregation is equivalent to existing privilege."); + } + + for (String aggrName : declaredNames) { + // aggregated privilege not registered + if (!definitions.containsKey(aggrName)) { + throw new CommitFailedException("Declared aggregate '"+ aggrName +"' is not a registered privilege."); + } + + // check for circular aggregation + if (isCircularAggregation(definition.getName(), aggrName)) { + String msg = "Detected circular aggregation within custom privilege caused by " + aggrName; + throw new CommitFailedException(msg); + } + } + + Set aggregateNames = resolveAggregates(declaredNames); + for (PrivilegeDefinition existing : definitions.values()) { + Set existingDeclared = existing.getDeclaredAggregateNames(); + if (existingDeclared.isEmpty()) { + continue; + } + + // test for exact same aggregation or aggregation with the same net effect + if (declaredNames.equals(existingDeclared) || aggregateNames.equals(resolveAggregates(existingDeclared))) { + String msg = "Custom aggregate privilege '" + definition.getName() + "' is already covered by '" + existing.getName() + '\''; + throw new CommitFailedException(msg); + } + } + } + + private boolean isCircularAggregation(String privilegeName, String aggregateName) { + if (privilegeName.equals(aggregateName)) { + return true; + } + + PrivilegeDefinition aggrPriv = definitions.get(aggregateName); + if (aggrPriv.getDeclaredAggregateNames().isEmpty()) { + return false; + } else { + boolean isCircular = false; + for (String name : aggrPriv.getDeclaredAggregateNames()) { + if (privilegeName.equals(name)) { + return true; + } + if (definitions.containsKey(name)) { + isCircular = isCircularAggregation(privilegeName, name); + } + } + return isCircular; + } + } + + private Set resolveAggregates(Set declared) throws CommitFailedException { + Set aggregateNames = new HashSet(); + for (String name : declared) { + PrivilegeDefinition d = definitions.get(name); + if (d == null) { + throw new CommitFailedException("Invalid declared aggregate name " + name + ": Unknown privilege."); + } + + Set names = d.getDeclaredAggregateNames(); + if (names.isEmpty()) { + aggregateNames.add(name); + } else { + aggregateNames.addAll(resolveAggregates(names)); + } + } + return aggregateNames; + } + + private void checkInitialized() throws CommitFailedException { + if (reader == null || definitions == null) { + throw new CommitFailedException(new IllegalStateException("Mandatory privileges root is missing.")); + } + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeValidatorProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeValidatorProvider.java new file mode 100644 index 00000000000..88a0cc39bf8 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/privilege/PrivilegeValidatorProvider.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.security.privilege; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.core.ReadOnlyTree; +import org.apache.jackrabbit.oak.spi.commit.SubtreeValidator; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import static org.apache.jackrabbit.JcrConstants.JCR_SYSTEM; +import static org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants.REP_PRIVILEGES; + +/** + * {@code PrivilegeValidatorProvider} to construct a {@code Validator} instance + * to make sure modifications to the /jcr:system/rep:privileges tree are compliant + * with constraints applied for custom privileges. + */ +class PrivilegeValidatorProvider implements ValidatorProvider { + + @Nonnull + @Override + public Validator getRootValidator(NodeState before, NodeState after) { + return new SubtreeValidator(new PrivilegeValidator(new ReadOnlyTree(before)), JCR_SYSTEM, REP_PRIVILEGES); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableBaseProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableBaseProvider.java new file mode 100644 index 00000000000..25e8a52647f --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableBaseProvider.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.security.user; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.apache.jackrabbit.oak.spi.security.user.util.UserUtility; + +/** + * AuthorizableBaseProvider... TODO + */ +abstract class AuthorizableBaseProvider implements UserConstants { + + final ConfigurationParameters config; + final Root root; + final IdentifierManager identifierManager; + + AuthorizableBaseProvider(Root root, ConfigurationParameters config) { + this.root = root; + this.config = config; + this.identifierManager = new IdentifierManager(root); + } + + Tree getByID(String authorizableId, AuthorizableType authorizableType) { + Tree tree = identifierManager.getTree(getContentID(authorizableId)); + if (UserUtility.isType(tree, authorizableType)) { + return tree; + } else { + return null; + } + } + + Tree getByPath(String authorizableOakPath) { + Tree tree = root.getTree(authorizableOakPath); + if (UserUtility.isType(tree, AuthorizableType.AUTHORIZABLE)) { + return tree; + } else { + return null; + } + } + + String getContentID(Tree authorizableTree) { + return identifierManager.getIdentifier(authorizableTree); + } + + static String getContentID(String authorizableId) { + return IdentifierManager.generateUUID(authorizableId.toLowerCase()); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableImpl.java new file mode 100644 index 00000000000..7b5c6166a23 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableImpl.java @@ -0,0 +1,281 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user; + +import java.util.Collections; +import java.util.Iterator; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.jackrabbit.oak.api.Type.STRING; + +/** + * AuthorizableImpl... + */ +abstract class AuthorizableImpl implements Authorizable, UserConstants { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(AuthorizableImpl.class); + + private final String id; + private final String principalName; + private final UserManagerImpl userManager; + + private Node node; + private AuthorizableProperties properties; + private int hashCode; + + AuthorizableImpl(String id, Tree tree, UserManagerImpl userManager) throws RepositoryException { + checkValidTree(tree); + this.id = id; + if (tree.hasProperty(REP_PRINCIPAL_NAME)) { + principalName = tree.getProperty(REP_PRINCIPAL_NAME).getValue(STRING); + } else { + String msg = "Authorizable without principal name " + id; + log.warn(msg); + throw new RepositoryException(msg); + } + this.userManager = userManager; + } + + abstract void checkValidTree(Tree tree) throws RepositoryException; + + static boolean isValidAuthorizableImpl(Authorizable authorizable) { + return authorizable instanceof AuthorizableImpl; + } + + //-------------------------------------------------------< Authorizable >--- + @Override + public String getID() { + return id; + } + + @Override + public Iterator declaredMemberOf() throws RepositoryException { + return getMembership(false); + } + + @Override + public Iterator memberOf() throws RepositoryException { + return getMembership(true); + } + + @Override + public void remove() throws RepositoryException { + // don't allow for removal of the administrator even if the executing + // session has all permissions. + if (!isGroup() && ((User) this).isAdmin()) { + throw new RepositoryException("The administrator cannot be removed."); + } + userManager.onRemove(this); + getTree().remove(); + } + + @Override + public Iterator getPropertyNames() throws RepositoryException { + return getPropertyNames("."); + } + + @Override + public Iterator getPropertyNames(String relPath) throws RepositoryException { + return getAuthorizableProperties().getNames(relPath); + } + + @Override + public boolean hasProperty(String relPath) throws RepositoryException { + return getAuthorizableProperties().hasProperty(relPath); + } + + @Override + public Value[] getProperty(String relPath) throws RepositoryException { + return getAuthorizableProperties().getProperty(relPath); + } + + @Override + public void setProperty(String relPath, Value value) throws RepositoryException { + getAuthorizableProperties().setProperty(relPath, value); + } + + @Override + public void setProperty(String relPath, Value[] values) throws RepositoryException { + getAuthorizableProperties().setProperty(relPath, values); + } + + @Override + public boolean removeProperty(String relPath) throws RepositoryException { + return getAuthorizableProperties().removeProperty(relPath); + } + + @Override + public String getPath() throws RepositoryException { + Node n = getNode(); + if (n != null) { + return n.getPath(); + } else { + return userManager.getNamePathMapper().getJcrPath(getTree().getPath()); + } + } + + //-------------------------------------------------------------< Object >--- + @Override + public int hashCode() { + if (hashCode == 0) { + // FIXME: add proper hash-code generation taking repo/workspace/tree-identifier into account +// try { +// Node node = getNode(); + StringBuilder sb = new StringBuilder(); + sb.append(isGroup() ? "group:" : "user:"); + //sb.append(node.getSession().getWorkspace().getName()); + sb.append(':'); + sb.append(id); + hashCode = sb.toString().hashCode(); +// } catch (RepositoryException e) { +// log.warn("Error while calculating hash code.",e.getMessage()); +// } + } + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof AuthorizableImpl) { + AuthorizableImpl otherAuth = (AuthorizableImpl) obj; + // FIXME: make sure 2 authorizables are based on the same tree/node object + return isGroup() == otherAuth.isGroup() && id.equals(otherAuth.id); + } + return false; + } + + @Override + public String toString() { + String typeStr = (isGroup()) ? "Group '" : "User '"; + return new StringBuilder().append(typeStr).append(id).append('\'').toString(); + } + + //-------------------------------------------------------------------------- + @Nonnull + Tree getTree() { + return userManager.getAuthorizableTree(id); + } + + @Nonnull + String getPrincipalName() throws RepositoryException { + return principalName; + } + + @CheckForNull + String getJcrName(String oakName) { + return userManager.getNamePathMapper().getJcrName(oakName); + } + + /** + * @return The user manager associated with this authorizable. + */ + @Nonnull + UserManagerImpl getUserManager() { + return userManager; + } + + /** + * @return The membership provider associated with this authorizable + */ + @Nonnull + MembershipProvider getMembershipProvider() { + return userManager.getMembershipProvider(); + } + + /** + * Returns {@code true} if this authorizable represents the 'everyone' group. + * + * @return {@code true} if this authorizable represents the group everyone + * is member of; {@code false} otherwise. + * @throws RepositoryException If an error occurs. + */ + boolean isEveryone() throws RepositoryException { + return isGroup() && EveryonePrincipal.NAME.equals(getPrincipalName()); + } + + /** + * @return The node associated with this authorizable instance. + * @throws javax.jcr.RepositoryException + */ + @CheckForNull + private Node getNode() throws RepositoryException { + if (node == null) { + node = userManager.getAuthorizableNode(id); + } + return node; + } + + /** + * Retrieve authorizable properties for property related operations. + * + * @return + * @throws RepositoryException + */ + private AuthorizableProperties getAuthorizableProperties() throws RepositoryException { + if (properties == null) { + properties = userManager.getAuthorizableProperties(id); + } + return properties; + } + + /** + * Retrieve the group membership of this authorizable. + * + * @param includeInherited Flag indicating whether the resulting iterator only + * contains groups this authorizable is declared member of or if inherited + * group membership is respected. + * + * @return Iterator of groups this authorizable is (declared) member of. + * @throws RepositoryException If an error occurs. + */ + @Nonnull + private Iterator getMembership(boolean includeInherited) throws RepositoryException { + if (isEveryone()) { + return Collections.emptySet().iterator(); + } + + MembershipProvider mMgr = getMembershipProvider(); + Iterator oakPaths = mMgr.getMembership(getTree(), includeInherited); + if (oakPaths.hasNext()) { + AuthorizableIterator groups = AuthorizableIterator.create(oakPaths, userManager, AuthorizableType.GROUP); + return new RangeIteratorAdapter(groups, groups.getSize()); + } else { + return RangeIteratorAdapter.EMPTY; + } + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableIterator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableIterator.java new file mode 100644 index 00000000000..2b6ea64e8fc --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableIterator.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user; + +import java.util.Iterator; +import javax.jcr.RangeIterator; +import javax.jcr.RepositoryException; + +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.Iterators; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AuthorizableIterator... + */ +class AuthorizableIterator implements Iterator { + + private static final Logger log = LoggerFactory.getLogger(AuthorizableIterator.class); + + private final Iterator authorizables; + private final long size; + + private Authorizable next; + + static AuthorizableIterator create(Iterator authorizableOakPaths, + UserManagerImpl userManager, + AuthorizableType authorizableType) { + Iterator it = Iterators.transform(authorizableOakPaths, new PathToAuthorizable(userManager, authorizableType)); + long size = getSize(authorizableOakPaths); + return new AuthorizableIterator(Iterators.filter(it, Predicates.notNull()), size); + } + + static AuthorizableIterator create(Iterator authorizableTrees, UserManagerImpl userManager) { + Iterator it = Iterators.transform(authorizableTrees, new TreeToAuthorizable(userManager)); + long size = getSize(authorizableTrees); + + return new AuthorizableIterator(Iterators.filter(it, Predicates.notNull()), size); + } + + private AuthorizableIterator(Iterator authorizables, long size) { + this.authorizables = authorizables; + this.size = size; + } + + //-----------------------------------------------------------< Iterator >--- + @Override + public boolean hasNext() { + return authorizables.hasNext(); + } + + @Override + public Authorizable next() { + return authorizables.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + //-------------------------------------------------------------------------- + long getSize() { + return size; + } + + //-------------------------------------------------------------------------- + + private static long getSize(Iterator it) { + if (it instanceof RangeIterator) { + return ((RangeIterator) it).getSize(); + } else { + return -1; + } + } + + private static class PathToAuthorizable implements Function { + + private final UserManagerImpl userManager; + private final Predicate predicate; + + public PathToAuthorizable(UserManagerImpl userManager, AuthorizableType type) { + this.userManager = userManager; + this.predicate = new AuthorizableTypePredicate(type); + } + + @Override + public Authorizable apply(String oakPath) { + String jcrPath = userManager.getNamePathMapper().getJcrPath(oakPath); + try { + Authorizable a = userManager.getAuthorizableByPath(jcrPath); + if (predicate.apply(a)) { + return a; + } + } catch (RepositoryException e) { + log.debug("Failed to access authorizable " + jcrPath); + } + return null; + } + } + + private static class TreeToAuthorizable implements Function { + + private final UserManagerImpl userManager; + + public TreeToAuthorizable(UserManagerImpl userManager) { + this.userManager = userManager; + } + + @Override + public Authorizable apply(Tree authorizableTree) { + try { + return userManager.getAuthorizable(authorizableTree); + } catch (RepositoryException e) { + log.debug("Failed to access authorizable " + authorizableTree.getPath()); + return null; + } + } + } + + private static class AuthorizableTypePredicate implements Predicate { + + private final AuthorizableType authorizableType; + + AuthorizableTypePredicate(AuthorizableType authorizableType) { + this.authorizableType = authorizableType; + } + + @Override + public boolean apply(Authorizable authorizable) { + return authorizableType.isType(authorizable); + } + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableProperties.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableProperties.java new file mode 100644 index 00000000000..ae380719810 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableProperties.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.security.user; + +import java.util.Iterator; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +/** + * AuthorizableProperty... TODO + */ +interface AuthorizableProperties { + + Iterator getNames(String relPath) throws RepositoryException; + + boolean hasProperty(String relPath) throws RepositoryException; + + Value[] getProperty(String relPath) throws RepositoryException; + + void setProperty(String relPath, Value value) throws RepositoryException; + + void setProperty(String relPath, Value[] values) throws RepositoryException; + + boolean removeProperty(String relPath) throws RepositoryException; + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/CredentialsImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/CredentialsImpl.java new file mode 100644 index 00000000000..d0e971b3b5f --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/CredentialsImpl.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.security.user; + +import javax.jcr.Credentials; + +/** + * CredentialsImpl... TODO + */ +public class CredentialsImpl implements Credentials { + + private final String userId; + private final String pwHash; + + CredentialsImpl(String userId, String pwHash) { + this.userId = userId; + this.pwHash = pwHash; + } + + public String getUserId() { + return userId; + } + + public String getPasswordHash() { + return pwHash; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/GroupImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/GroupImpl.java new file mode 100644 index 00000000000..3e28c15e427 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/GroupImpl.java @@ -0,0 +1,260 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user; + +import java.security.Principal; +import java.util.Enumeration; +import java.util.Iterator; +import javax.annotation.Nullable; +import javax.jcr.RepositoryException; + +import com.google.common.base.Function; +import com.google.common.collect.Iterators; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.oak.spi.security.principal.TreeBasedPrincipal; +import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType; +import org.apache.jackrabbit.oak.spi.security.user.util.UserUtility; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * GroupImpl... + */ +class GroupImpl extends AuthorizableImpl implements Group { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(GroupImpl.class); + + GroupImpl(String id, Tree tree, UserManagerImpl userManager) throws RepositoryException { + super(id, tree, userManager); + } + + //---------------------------------------------------< AuthorizableImpl >--- + @Override + void checkValidTree(Tree tree) throws RepositoryException { + if (tree == null || !UserUtility.isType(tree, AuthorizableType.GROUP)) { + throw new IllegalArgumentException("Invalid group node: node type rep:Group expected."); + } + } + + //-------------------------------------------------------< Authorizable >--- + @Override + public boolean isGroup() { + return true; + } + + @Override + public Principal getPrincipal() throws RepositoryException { + return new GroupPrincipal(getPrincipalName(), getTree()); + } + + //--------------------------------------------------------------< Group >--- + @Override + public Iterator getDeclaredMembers() throws RepositoryException { + return getMembers(false); + } + + @Override + public Iterator getMembers() throws RepositoryException { + return getMembers(true); + } + + @Override + public boolean isDeclaredMember(Authorizable authorizable) throws RepositoryException { + return isMember(authorizable, false); + } + + @Override + public boolean isMember(Authorizable authorizable) throws RepositoryException { + return isMember(authorizable, true); + } + + @Override + public boolean addMember(Authorizable authorizable) throws RepositoryException { + if (!isValidAuthorizableImpl(authorizable)) { + log.warn("Invalid Authorizable: {}", authorizable); + return false; + } + + AuthorizableImpl authorizableImpl = ((AuthorizableImpl) authorizable); + if (isEveryone() || authorizableImpl.isEveryone()) { + return false; + } + + String memberID = authorizable.getID(); + if (authorizableImpl.isGroup()) { + if (getID().equals(memberID)) { + String msg = "Attempt to add a group as member of itself (" + getID() + ")."; + log.debug(msg); + return false; + } + if (((Group) authorizableImpl).isMember(this)) { + log.debug("Attempt to create circular group membership."); + return false; + } + } + + if (isDeclaredMember(authorizable)) { + log.debug("Authorizable {} is already declared member of {}", memberID, getID()); + return false; + } + + return getMembershipProvider().addMember(getTree(), authorizableImpl.getTree()); + } + + @Override + public boolean removeMember(Authorizable authorizable) throws RepositoryException { + if (!isValidAuthorizableImpl(authorizable)) { + log.warn("Invalid Authorizable: {}", authorizable); + return false; + } + if (isEveryone()) { + return false; + } else { + Tree memberTree = ((AuthorizableImpl) authorizable).getTree(); + return getMembershipProvider().removeMember(getTree(), memberTree); + } + } + + //-------------------------------------------------------------------------- + /** + * Internal implementation of {@link #getDeclaredMembers()} and {@link #getMembers()}. + * + * @param includeInherited Flag indicating if only the declared or all members + * should be returned. + * @return Iterator of authorizables being member of this group. + * @throws RepositoryException If an error occurs. + */ + private Iterator getMembers(boolean includeInherited) throws RepositoryException { + UserManagerImpl userMgr = getUserManager(); + if (isEveryone()) { + String propName = getJcrName(REP_PRINCIPAL_NAME); + return userMgr.findAuthorizables(propName, null, UserManager.SEARCH_TYPE_AUTHORIZABLE); + } else { + Iterator oakPaths = getMembershipProvider().getMembers(getTree(), AuthorizableType.AUTHORIZABLE, includeInherited); + if (oakPaths.hasNext()) { + AuthorizableIterator iterator = AuthorizableIterator.create(oakPaths, userMgr, AuthorizableType.AUTHORIZABLE); + return new RangeIteratorAdapter(iterator, iterator.getSize()); + } else { + return RangeIteratorAdapter.EMPTY; + } + } + } + + /** + * Internal implementation of {@link #isDeclaredMember(Authorizable)} and {@link #isMember(Authorizable)}. + * + * @param authorizable The authorizable to test. + * @param includeInherited Flag indicating if only declared or all members + * should taken into account. + * @return {@code true} if the specified authorizable is member or declared + * member of this group; {@code false} otherwise. + * @throws RepositoryException If an error occurs. + */ + private boolean isMember(Authorizable authorizable, boolean includeInherited) throws RepositoryException { + if (!isValidAuthorizableImpl(authorizable)) { + return false; + } + + if (isEveryone()) { + return true; + } else if (getID().equals(authorizable.getID())) { + return false; + } else { + Tree authorizableTree = ((AuthorizableImpl) authorizable).getTree(); + MembershipProvider mgr = getUserManager().getMembershipProvider(); + return mgr.isMember(this.getTree(), authorizableTree, includeInherited); + } + } + + /** + * Principal representation of this group instance. + */ + private class GroupPrincipal extends TreeBasedPrincipal implements java.security.acl.Group { + + GroupPrincipal(String principalName, Tree groupTree) { + super(principalName, groupTree, getUserManager().getNamePathMapper()); + } + + @Override + public boolean addMember(Principal principal) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeMember(Principal principal) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isMember(Principal principal) { + boolean isMember = false; + try { + // shortcut for everyone group -> avoid collecting all members + // as all users and groups are member of everyone. + if (isEveryone()) { + isMember = !EveryonePrincipal.NAME.equals(principal.getName()); + } else { + Authorizable a = getUserManager().getAuthorizable(principal); + if (a != null) { + isMember = GroupImpl.this.isMember(a); + } + } + } catch (RepositoryException e) { + log.warn("Failed to determine group membership", e.getMessage()); + } + + // principal doesn't represent a known authorizable or an error occurred. + return isMember; + } + + @Override + public Enumeration members() { + final Iterator members; + try { + members = GroupImpl.this.getMembers(); + } catch (RepositoryException e) { + // should not occur. + String msg = "Unable to retrieve Group members: " + e.getMessage(); + log.error(msg); + throw new IllegalStateException(msg); + } + + Iterator principals = Iterators.transform(members, new Function() { + @Override + public Principal apply(@Nullable Authorizable authorizable) { + assert authorizable != null; + try { + return authorizable.getPrincipal(); + } catch (RepositoryException e) { + String msg = "Internal error while retrieving principal: " + e.getMessage(); + log.error(msg); + throw new IllegalStateException(msg); + } + } + }); + return Iterators.asEnumeration(principals); + } + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/ImpersonationImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/ImpersonationImpl.java new file mode 100644 index 00000000000..742a37b9006 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/ImpersonationImpl.java @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user; + +import java.security.Principal; +import java.security.acl.Group; +import java.util.HashSet; +import java.util.Set; +import javax.jcr.RepositoryException; +import javax.security.auth.Subject; + +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Impersonation; +import org.apache.jackrabbit.api.security.user.User; +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.spi.security.principal.AdminPrincipal; +import org.apache.jackrabbit.oak.spi.security.principal.PrincipalIteratorAdapter; +import org.apache.jackrabbit.oak.spi.security.principal.PrincipalProvider; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.jackrabbit.oak.api.Type.STRINGS; + +/** + * ImpersonationImpl... + */ +class ImpersonationImpl implements Impersonation, UserConstants { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(ImpersonationImpl.class); + + private final UserImpl user; + private final PrincipalProvider principalProvider; + + ImpersonationImpl(UserImpl user) throws RepositoryException { + this.user = user; + this.principalProvider = user.getUserManager().getPrincipalProvider(); + } + + //------------------------------------------------------< Impersonation >--- + /** + * @see org.apache.jackrabbit.api.security.user.Impersonation#getImpersonators() + */ + @Override + public PrincipalIterator getImpersonators() throws RepositoryException { + Set impersonators = getImpersonatorNames(); + if (impersonators.isEmpty()) { + return PrincipalIteratorAdapter.EMPTY; + } else { + Set s = new HashSet(); + for (final String pName : impersonators) { + Principal p = principalProvider.getPrincipal(pName); + if (p == null) { + log.debug("Impersonator " + pName + " does not correspond to a known Principal."); + p = new Principal() { + @Override + public String getName() { + return pName; + } + }; + } + s.add(p); + + } + return new PrincipalIteratorAdapter(s); + } + } + + /** + * @see org.apache.jackrabbit.api.security.user.Impersonation#grantImpersonation(Principal) + */ + @Override + public boolean grantImpersonation(Principal principal) throws RepositoryException { + String principalName = principal.getName(); + Principal p = principalProvider.getPrincipal(principalName); + if (p == null) { + log.debug("Cannot grant impersonation to an unknown principal."); + return false; + } + if (p instanceof Group) { + log.debug("Cannot grant impersonation to a principal that is a Group."); + return false; + } + + // make sure user does not impersonate himself + Tree userTree = user.getTree(); + PropertyState prop = userTree.getProperty(REP_PRINCIPAL_NAME); + if (prop != null && prop.getValue(Type.STRING).equals(principalName)) { + log.warn("Cannot grant impersonation to oneself."); + return false; + } + + // make sure the given principal doesn't refer to the admin user. + if (isAdmin(p)) { + log.debug("Admin principal is already granted impersonation."); + return false; + } + + Set impersonators = getImpersonatorNames(userTree); + if (impersonators.add(principalName)) { + updateImpersonatorNames(userTree, impersonators); + return true; + } else { + return false; + } + } + + /** + * @see Impersonation#revokeImpersonation(java.security.Principal) + */ + @Override + public boolean revokeImpersonation(Principal principal) throws RepositoryException { + String pName = principal.getName(); + + Tree userTree = user.getTree(); + Set impersonators = getImpersonatorNames(userTree); + if (impersonators.remove(pName)) { + updateImpersonatorNames(userTree, impersonators); + return true; + } else { + return false; + } + } + + /** + * @see Impersonation#allows(javax.security.auth.Subject) + */ + @Override + public boolean allows(Subject subject) throws RepositoryException { + if (subject == null) { + return false; + } + + Set principalNames = new HashSet(); + for (Principal principal : subject.getPrincipals()) { + principalNames.add(principal.getName()); + } + + boolean allows = getImpersonatorNames().removeAll(principalNames); + if (!allows) { + // check if subject belongs to administrator user + for (Principal principal : subject.getPrincipals()) { + if (isAdmin(principal)) { + allows = true; + break; + } + } + } + return allows; + } + + //------------------------------------------------------------< private >--- + private Set getImpersonatorNames() throws RepositoryException { + return getImpersonatorNames(user.getTree()); + } + + private Set getImpersonatorNames(Tree userTree) { + Set princNames = new HashSet(); + PropertyState impersonators = userTree.getProperty(REP_IMPERSONATORS); + if (impersonators != null) { + for (String v : impersonators.getValue(STRINGS)) { + princNames.add(v); + } + } + return princNames; + } + + private void updateImpersonatorNames(Tree userTree, Set principalNames) { + if (principalNames == null || principalNames.isEmpty()) { + userTree.removeProperty(REP_IMPERSONATORS); + } else { + userTree.setProperty(REP_IMPERSONATORS, principalNames, Type.STRINGS); + } + } + + private boolean isAdmin(Principal principal) { + if (principal instanceof AdminPrincipal) { + return true; + } else if (principal instanceof Group) { + return false; + } else { + try { + Authorizable authorizable = user.getUserManager().getAuthorizable(principal); + return authorizable != null && !authorizable.isGroup() && ((User) authorizable).isAdmin(); + } catch (RepositoryException e) { + log.debug(e.getMessage()); + return false; + } + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/JcrAuthorizableProperties.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/JcrAuthorizableProperties.java new file mode 100644 index 00000000000..545bd38ac0d --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/JcrAuthorizableProperties.java @@ -0,0 +1,246 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import javax.annotation.Nonnull; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * JcrAuthorizableProperty... TODO + */ +class JcrAuthorizableProperties implements AuthorizableProperties, UserConstants { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(JcrAuthorizableProperties.class); + + private final Node authorizableNode; + private final NamePathMapper namePathMapper; + + JcrAuthorizableProperties(Node authorizableNode, NamePathMapper namePathMapper) { + this.authorizableNode = authorizableNode; + this.namePathMapper = namePathMapper; + } + + @Override + public Iterator getNames(String relPath) throws RepositoryException { + Node node = getNode(); + Node n = node.getNode(relPath); + if (Text.isDescendantOrEqual(node.getPath(), n.getPath())) { + List l = new ArrayList(); + for (PropertyIterator it = n.getProperties(); it.hasNext();) { + Property prop = it.nextProperty(); + if (isAuthorizableProperty(prop, false)) { + l.add(prop.getName()); + } + } + return l.iterator(); + } else { + throw new IllegalArgumentException("Relative path " + relPath + " refers to items outside of scope of authorizable."); + } + } + + /** + * @see org.apache.jackrabbit.api.security.user.Authorizable#hasProperty(String) + */ + @Override + public boolean hasProperty(String relPath) throws RepositoryException { + Node node = getNode(); + return node.hasProperty(relPath) && isAuthorizableProperty(node.getProperty(relPath), true); + } + + /** + * @see org.apache.jackrabbit.api.security.user.Authorizable#getProperty(String) + */ + @Override + public Value[] getProperty(String relPath) throws RepositoryException { + Node node = getNode(); + Value[] values = null; + if (node.hasProperty(relPath)) { + Property prop = node.getProperty(relPath); + if (isAuthorizableProperty(prop, true)) { + if (prop.isMultiple()) { + values = prop.getValues(); + } else { + values = new Value[]{prop.getValue()}; + } + } + } + return values; + } + + /** + * @see org.apache.jackrabbit.api.security.user.Authorizable#setProperty(String, javax.jcr.Value) + */ + @Override + public void setProperty(String relPath, Value value) throws RepositoryException { + String name = Text.getName(relPath); + String intermediate = (relPath.equals(name)) ? null : Text.getRelativeParent(relPath, 1); + + Node n = getOrCreateTargetNode(intermediate); + // check if the property has already been created as multi valued + // property before -> in this case remove in order to avoid + // ValueFormatException. + if (n.hasProperty(name)) { + Property p = n.getProperty(name); + if (p.isMultiple()) { + p.remove(); + } + } + n.setProperty(name, value); + } + + /** + * @see org.apache.jackrabbit.api.security.user.Authorizable#setProperty(String, javax.jcr.Value[]) + */ + @Override + public void setProperty(String relPath, Value[] values) throws RepositoryException { + String name = Text.getName(relPath); + String intermediate = (relPath.equals(name)) ? null : Text.getRelativeParent(relPath, 1); + + Node n = getOrCreateTargetNode(intermediate); + // check if the property has already been created as single valued + // property before -> in this case remove in order to avoid + // ValueFormatException. + if (n.hasProperty(name)) { + Property p = n.getProperty(name); + if (!p.isMultiple()) { + p.remove(); + } + } + n.setProperty(name, values); + } + + /** + * @see org.apache.jackrabbit.api.security.user.Authorizable#removeProperty(String) + */ + @Override + public boolean removeProperty(String relPath) throws RepositoryException { + Node node = getNode(); + if (node.hasProperty(relPath)) { + Property p = node.getProperty(relPath); + if (isAuthorizableProperty(p, true)) { + p.remove(); + return true; + } else { + throw new ConstraintViolationException("Property " + relPath + " isn't a modifiable authorizable property"); + } + } + // no such property or wasn't a property of this authorizable. + return false; + } + + private Node getNode() { + return authorizableNode; + } + + private String getJcrName(String oakName) { + return namePathMapper.getJcrName(oakName); + } + + /** + * Returns true if the given property of the authorizable node is one of the + * non-protected properties defined by the rep:Authorizable node type or a + * some other descendant of the authorizable node. + * + * @param prop Property to be tested. + * @param verifyAncestor If true the property is tested to be a descendant + * of the node of this authorizable; otherwise it is expected that this + * test has been executed by the caller. + * @return {@code true} if the given property is defined + * by the rep:authorizable node type or one of it's sub-node types; + * {@code false} otherwise. + * @throws RepositoryException If the property definition cannot be retrieved. + */ + private boolean isAuthorizableProperty(Property prop, boolean verifyAncestor) throws RepositoryException { + Node node = getNode(); + if (verifyAncestor && !Text.isDescendant(node.getPath(), prop.getPath())) { + log.debug("Attempt to access property outside of authorizable scope."); + return false; + } + + PropertyDefinition def = prop.getDefinition(); + if (def.isProtected()) { + return false; + } else if (node.isSame(prop.getParent())) { + NodeType declaringNt = prop.getDefinition().getDeclaringNodeType(); + return declaringNt.isNodeType(getJcrName(NT_REP_AUTHORIZABLE)); + } else { + // another non-protected property somewhere in the subtree of this + // authorizable node -> is a property that can be set using #setProperty. + return true; + } + } + + /** + * Retrieves the node at {@code relPath} relative to node associated with + * this authorizable. If no such node exist it and any missing intermediate + * nodes are created. + * + * @param relPath A relative path. + * @return The corresponding node. + * @throws RepositoryException If an error occurs or if {@code relPath} refers + * to a node that is outside of the scope of this authorizable. + */ + @Nonnull + private Node getOrCreateTargetNode(String relPath) throws RepositoryException { + Node n; + Node node = getNode(); + if (relPath != null) { + String userPath = node.getPath(); + if (node.hasNode(relPath)) { + n = node.getNode(relPath); + if (!Text.isDescendantOrEqual(userPath, n.getPath())) { + throw new RepositoryException("Relative path " + relPath + " outside of scope of " + this); + } + } else { + n = node; + for (String segment : Text.explode(relPath, '/')) { + if (n.hasNode(segment)) { + n = n.getNode(segment); + } else { + if (Text.isDescendantOrEqual(userPath, n.getPath())) { + n = n.addNode(segment); + } else { + throw new RepositoryException("Relative path " + relPath + " outside of scope of " + this); + } + } + } + } + } else { + n = node; + } + return n; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/MembershipProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/MembershipProvider.java new file mode 100644 index 00000000000..fb147bf4978 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/MembershipProvider.java @@ -0,0 +1,323 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.jcr.PropertyType; + +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; +import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter; +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.plugins.memory.MemoryPropertyBuilder; +import org.apache.jackrabbit.oak.plugins.memory.PropertyStates; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType; +import org.apache.jackrabbit.oak.spi.security.user.util.UserUtility; +import org.apache.jackrabbit.oak.spi.state.PropertyBuilder; +import org.apache.jackrabbit.oak.util.NodeUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.jackrabbit.oak.api.Type.STRINGS; +import static org.apache.jackrabbit.oak.api.Type.WEAKREFERENCE; + +/** + * {@code MembershipProvider} implementation storing group membership information + * with the {@code Tree} associated with a given {@link org.apache.jackrabbit.api.security.user.Group}. + * Depending on the configuration there are two variants on how group members + * are recorded: + * + *

Membership stored in multi-valued property

+ * This is the default way of storing membership information with the following + * characteristics: + *
    + *
  • Multivalued property {@link #REP_MEMBERS}
  • + *
  • Property type: {@link PropertyType#WEAKREFERENCE}
  • + *
  • Used if the config option {@link org.apache.jackrabbit.oak.spi.security.user.UserConstants#PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE} is missing or <4
  • + *
+ * + *

Membership stored in individual properties

+ * Variant to store group membership based on the + * {@link org.apache.jackrabbit.oak.spi.security.user.UserConstants#PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE} configuration parameter: + * + *
    + *
  • Membership information stored underneath a {@link #REP_MEMBERS} node hierarchy
  • + *
  • Individual member information is stored each in a {@link PropertyType#WEAKREFERENCE} + * property
  • + *
  • Node hierarchy is split based on the {@link org.apache.jackrabbit.oak.spi.security.user.UserConstants#PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE} + * configuration parameter.
  • + *
  • {@link org.apache.jackrabbit.oak.spi.security.user.UserConstants#PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE} must be greater than 4 + * in order to turn on this behavior
  • + *
+ * + *

Compatibility

+ * This membership provider is able to deal with both options being present in + * the content. If the {@link org.apache.jackrabbit.oak.spi.security.user.UserConstants#PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE} configuration + * parameter is modified later on, existing membership information is not + * modified or converted to the new structure. + */ +class MembershipProvider extends AuthorizableBaseProvider { + + private static final Logger log = LoggerFactory.getLogger(MembershipProvider.class); + + private final int splitSize; + + MembershipProvider(Root root, ConfigurationParameters config) { + super(root, config); + + int splitValue = config.getConfigValue(PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE, 0); + if (splitValue != 0 && splitValue < 4) { + log.warn("Invalid value {} for {}. Expected integer >= 4 or 0", splitValue, PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE); + splitValue = 0; + } + this.splitSize = splitValue; + } + + @Nonnull + Iterator getMembership(Tree authorizableTree, boolean includeInherited) { + Set groupPaths = new HashSet(); + Set refPaths = identifierManager.getReferences(true, authorizableTree, REP_MEMBERS, NT_REP_GROUP, NT_REP_MEMBERS); + for (String propPath : refPaths) { + int index = propPath.indexOf('/'+REP_MEMBERS); + if (index > 0) { + groupPaths.add(propPath.substring(0, index)); + } else { + log.debug("Not a membership reference property " + propPath); + } + } + + Iterator it = groupPaths.iterator(); + if (includeInherited && it.hasNext()) { + return getAllMembership(groupPaths.iterator()); + } else { + return new RangeIteratorAdapter(it, groupPaths.size()); + } + } + + @Nonnull + Iterator getMembers(Tree groupTree, AuthorizableType authorizableType, boolean includeInherited) { + Iterable memberPaths = Collections.emptySet(); + if (useMemberNode(groupTree)) { + Tree membersTree = groupTree.getChild(REP_MEMBERS); + if (membersTree != null) { + throw new UnsupportedOperationException("not implemented: retrieve members from member-node hierarchy"); + } + } else { + PropertyState property = groupTree.getProperty(REP_MEMBERS); + if (property != null) { + Iterable vs = property.getValue(STRINGS); + memberPaths = Iterables.transform(vs, new Function() { + @Override + public String apply(@Nullable String value) { + return identifierManager.getPath(PropertyStates.createProperty("", value, WEAKREFERENCE)); + } + }); + } + } + + Iterator it = memberPaths.iterator(); + if (includeInherited && it.hasNext()) { + return getAllMembers(it, authorizableType); + } else { + return new RangeIteratorAdapter(it, Iterables.size(memberPaths)); + } + } + + boolean isMember(Tree groupTree, Tree authorizableTree, boolean includeInherited) { + if (includeInherited) { + Iterator groupPaths = getMembership(authorizableTree, true); + String path = groupTree.getPath(); + while (groupPaths.hasNext()) { + if (path.equals(groupPaths.next())) { + return true; + } + } + } else { + if (useMemberNode(groupTree)) { + Tree membersTree = groupTree.getChild(REP_MEMBERS); + if (membersTree != null) { + // FIXME: fix.. testing for property name in jr2 wasn't correct. + // TODO: add implementation + throw new UnsupportedOperationException("not implemented: isMembers determined from member-node hierarchy"); + } + } else { + PropertyState property = groupTree.getProperty(REP_MEMBERS); + if (property != null) { + Iterable members = property.getValue(STRINGS); + String authorizableUUID = getContentID(authorizableTree); + for (String v : members) { + if (authorizableUUID.equals(v)) { + return true; + } + } + } + } + } + // no a member of the specified group + return false; + } + + boolean addMember(Tree groupTree, Tree newMemberTree) { + if (useMemberNode(groupTree)) { + NodeUtil groupNode = new NodeUtil(groupTree); + NodeUtil membersNode = groupNode.getOrAddChild(REP_MEMBERS, NT_REP_MEMBERS); + // TODO: add implementation + throw new UnsupportedOperationException("not implemented: addMember with member-node hierarchy"); + } else { + String toAdd = getContentID(newMemberTree); + PropertyState property = groupTree.getProperty(REP_MEMBERS); + PropertyBuilder propertyBuilder = property == null + ? MemoryPropertyBuilder.create(WEAKREFERENCE, REP_MEMBERS) + : MemoryPropertyBuilder.create(WEAKREFERENCE, property); + if (propertyBuilder.hasValue(toAdd)) { + return false; + } else { + propertyBuilder.addValue(toAdd); + } + groupTree.setProperty(propertyBuilder.getPropertyState(true)); + } + return true; + } + + boolean removeMember(Tree groupTree, Tree memberTree) { + if (useMemberNode(groupTree)) { + Tree membersTree = groupTree.getChild(REP_MEMBERS); + if (membersTree != null) { + // TODO: add implementation + throw new UnsupportedOperationException("not implemented: remove member from member-node hierarchy"); + } + } else { + String toRemove = getContentID(memberTree); + PropertyState property = groupTree.getProperty(REP_MEMBERS); + PropertyBuilder propertyBuilder = property == null + ? MemoryPropertyBuilder.create(WEAKREFERENCE, REP_MEMBERS) + : MemoryPropertyBuilder.create(WEAKREFERENCE, property); + if (propertyBuilder.hasValue(toRemove)) { + propertyBuilder.removeValue(toRemove); + if (propertyBuilder.isEmpty()) { + groupTree.removeProperty(REP_MEMBERS); + } else { + groupTree.setProperty(propertyBuilder.getPropertyState(true)); + } + return true; + } + } + + // nothing changed + log.debug("Authorizable {} was not member of {}", memberTree.getName(), groupTree.getName()); + return false; + } + + //-----------------------------------------< private MembershipProvider >--- + + private boolean useMemberNode(Tree groupTree) { + return splitSize >= 4 && !groupTree.hasProperty(REP_MEMBERS); + } + + /** + * Returns an iterator of authorizables which includes all indirect members + * of the given iterator of authorizables. + * + * + * @param declaredMembers Iterator containing the paths to the declared members. + * @param authorizableType Flag used to filter the result by authorizable type. + * @return Iterator of Authorizable objects + */ + private Iterator getAllMembers(final Iterator declaredMembers, + final AuthorizableType authorizableType) { + Iterator> inheritedMembers = new Iterator>() { + @Override + public boolean hasNext() { + return declaredMembers.hasNext(); + } + + @Override + public Iterator next() { + String memberPath = declaredMembers.next(); + if (memberPath == null) { + return Iterators.emptyIterator(); + } else { + return Iterators.concat(Iterators.singletonIterator(memberPath), inherited(memberPath)); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + private Iterator inherited(String authorizablePath) { + Tree group = getByPath(authorizablePath); + if (UserUtility.isType(group, AuthorizableType.GROUP)) { + return getMembers(group, authorizableType, true); + } else { + return Iterators.emptyIterator(); + } + } + }; + return Iterators.filter(Iterators.concat(inheritedMembers), new ProcessedPathPredicate()); + } + + private Iterator getAllMembership(final Iterator groupPaths) { + Iterator> inheritedMembership = new Iterator>() { + @Override + public boolean hasNext() { + return groupPaths.hasNext(); + } + + @Override + public Iterator next() { + String groupPath = groupPaths.next(); + return Iterators.concat(Iterators.singletonIterator(groupPath), inherited(groupPath)); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + private Iterator inherited(String authorizablePath) { + Tree group = getByPath(authorizablePath); + if (UserUtility.isType(group, AuthorizableType.GROUP)) { + return getMembership(group, true); + } else { + return Iterators.emptyIterator(); + } + } + }; + + return Iterators.filter(Iterators.concat(inheritedMembership), new ProcessedPathPredicate()); + } + + private static final class ProcessedPathPredicate implements Predicate { + private final Set processed = new HashSet(); + @Override + public boolean apply(@Nullable String path) { + return processed.add(path); + } + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/OakAuthorizableProperties.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/OakAuthorizableProperties.java new file mode 100644 index 00000000000..f025fcbf8fd --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/OakAuthorizableProperties.java @@ -0,0 +1,254 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import javax.annotation.Nonnull; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.api.TreeLocation; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.plugins.name.NamespaceConstants; +import org.apache.jackrabbit.oak.plugins.value.ValueFactoryImpl; +import org.apache.jackrabbit.oak.util.NodeUtil; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * OakAuthorizableProperty... TODO + */ +class OakAuthorizableProperties implements AuthorizableProperties { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(OakAuthorizableProperties.class); + + private final UserProvider userProvider; + private final String id; + private final NamePathMapper namePathMapper; + + OakAuthorizableProperties(UserProvider userProvider, String id, NamePathMapper namePathMapper) { + this.userProvider = userProvider; + this.id = id; + this.namePathMapper = namePathMapper; + } + + @Override + public Iterator getNames(String relPath) throws RepositoryException { + Tree tree = getTree(); + Tree n = tree.getLocation().getChild(relPath).getTree(); + if (n != null && Text.isDescendantOrEqual(tree.getPath(), n.getPath())) { + List l = new ArrayList(); + for (PropertyState property : n.getProperties()) { + if (isAuthorizableProperty(tree, property)) { + l.add(property.getName()); + } + } + return l.iterator(); + } else { + throw new IllegalArgumentException("Relative path " + relPath + " refers to items outside of scope of authorizable."); + } + } + + /** + * @see org.apache.jackrabbit.api.security.user.Authorizable#hasProperty(String) + */ + @Override + public boolean hasProperty(String relPath) throws RepositoryException { + Tree tree = getTree(); + TreeLocation propertyLocation = getPropertyLocation(tree, relPath); + return propertyLocation.getProperty() != null && isAuthorizableProperty(tree, propertyLocation, true); + } + + /** + * @see org.apache.jackrabbit.api.security.user.Authorizable#getProperty(String) + */ + @Override + public Value[] getProperty(String relPath) throws RepositoryException { + Tree tree = getTree(); + Value[] values = null; + TreeLocation propertyLocation = getPropertyLocation(tree, relPath); + PropertyState property = propertyLocation.getProperty(); + if (property != null) { + if (isAuthorizableProperty(tree, propertyLocation, true)) { + if (property.isArray()) { + List vs = ValueFactoryImpl.createValues(property, namePathMapper); + values = vs.toArray(new Value[vs.size()]); + } else { + values = new Value[]{ValueFactoryImpl.createValue(property, namePathMapper)}; + } + } + } + return values; + } + + /** + * @see org.apache.jackrabbit.api.security.user.Authorizable#setProperty(String, javax.jcr.Value) + */ + @Override + public void setProperty(String relPath, Value value) throws RepositoryException { + String name = Text.getName(relPath); + String intermediate = (relPath.equals(name)) ? null : Text.getRelativeParent(relPath, 1); + + Tree n = getOrCreateTargetTree(intermediate); + // check if the property has already been created as multi valued + // property before -> in this case remove in order to avoid + // ValueFormatException. + if (n.hasProperty(name)) { + PropertyState p = n.getProperty(name); + if (p.isArray()) { + n.removeProperty(name); + } + } + n.setProperty(name, value); + } + + /** + * @see org.apache.jackrabbit.api.security.user.Authorizable#setProperty(String, javax.jcr.Value[]) + */ + @Override + public void setProperty(String relPath, Value[] values) throws RepositoryException { + String name = Text.getName(relPath); + String intermediate = (relPath.equals(name)) ? null : Text.getRelativeParent(relPath, 1); + + Tree n = getOrCreateTargetTree(intermediate); + // check if the property has already been created as single valued + // property before -> in this case remove in order to avoid + // ValueFormatException. + if (n.hasProperty(name)) { + PropertyState p = n.getProperty(name); + if (!p.isArray()) { + n.removeProperty(name); + } + } + n.setProperty(name, values); + } + + /** + * @see org.apache.jackrabbit.api.security.user.Authorizable#removeProperty(String) + */ + @Override + public boolean removeProperty(String relPath) throws RepositoryException { + Tree node = getTree(); + TreeLocation propertyLocation = node.getLocation().getChild(relPath); + PropertyState property = propertyLocation.getProperty(); + if (property != null) { + if (isAuthorizableProperty(node, propertyLocation, true)) { + Tree parent = propertyLocation.getParent().getTree(); + parent.removeProperty(property.getName()); + return true; + } + } + // no such property or wasn't a property of this authorizable. + return false; + } + + private Tree getTree() { + return userProvider.getAuthorizable(id); + } + + private String getJcrName(String oakName) { + return namePathMapper.getJcrName(oakName); + } + + /** + * Returns true if the given property of the authorizable node is one of the + * non-protected properties defined by the rep:Authorizable node type or a + * some other descendant of the authorizable node. + * + * @param authorizableTree The tree of the target authorizable. + * @param propertyLocation Location to be tested. + * @param verifyAncestor If true the property is tested to be a descendant + * of the node of this authorizable; otherwise it is expected that this + * test has been executed by the caller. + * @return {@code true} if the given property is defined + * by the rep:authorizable node type or one of it's sub-node types; + * {@code false} otherwise. + * @throws RepositoryException If the property definition cannot be retrieved. + */ + private boolean isAuthorizableProperty(Tree authorizableTree, TreeLocation propertyLocation, boolean verifyAncestor) throws RepositoryException { + if (verifyAncestor && !Text.isDescendant(authorizableTree.getPath(), propertyLocation.getPath())) { + log.debug("Attempt to access property outside of authorizable scope."); + return false; + } + return isAuthorizableProperty(authorizableTree, propertyLocation.getProperty()); + + + } + + private boolean isAuthorizableProperty(Tree authorizableTree, PropertyState property) throws RepositoryException { + // FIXME: add proper check for protection and declaring nt of the + // FIXME: property using nt functionality provided by nt-plugins + String prefix = Text.getNamespacePrefix(property.getName()); + return NamespaceConstants.RESERVED_PREFIXES.contains(prefix); + } + + /** + * Retrieves the node at {@code relPath} relative to node associated with + * this authorizable. If no such node exist it and any missing intermediate + * nodes are created. + * + * @param relPath A relative path. + * @return The corresponding node. + * @throws RepositoryException If an error occurs or if {@code relPath} refers + * to a node that is outside of the scope of this authorizable. + */ + @Nonnull + private Tree getOrCreateTargetTree(String relPath) throws RepositoryException { + Tree n; + Tree node = getTree(); + if (relPath != null) { + String userPath = node.getPath(); + n = node.getLocation().getChild(relPath).getTree(); + if (n != null) { + if (!Text.isDescendantOrEqual(userPath, n.getPath())) { + throw new RepositoryException("Relative path " + relPath + " outside of scope of " + this); + } + } else { + n = node; + for (String segment : Text.explode(relPath, '/')) { + if (n.hasChild(segment)) { + n = n.getChild(segment); + } else { + if (Text.isDescendantOrEqual(userPath, n.getPath())) { + NodeUtil util = new NodeUtil(n, namePathMapper); + n = util.addChild(segment, JcrConstants.NT_UNSTRUCTURED).getTree(); + } else { + throw new RepositoryException("Relative path " + relPath + " outside of scope of " + this); + } + } + } + } + } else { + n = node; + } + return n; + } + + @Nonnull + private TreeLocation getPropertyLocation(Tree tree, String relativePath) { + return tree.getLocation().getChild(relativePath); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java new file mode 100644 index 00000000000..344a8a426a3 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user; + +import java.util.Collections; +import java.util.List; +import javax.annotation.Nonnull; +import javax.jcr.Session; + +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.apache.jackrabbit.oak.spi.security.SecurityConfiguration; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration; +import org.apache.jackrabbit.oak.spi.security.user.action.AuthorizableAction; + +/** + * UserConfigurationImpl... TODO + */ +public class UserConfigurationImpl extends SecurityConfiguration.Default implements UserConfiguration { + + private final ConfigurationParameters config; + private final SecurityProvider securityProvider; + + public UserConfigurationImpl(ConfigurationParameters config, + SecurityProvider securityProvider) { + this.config = config; + this.securityProvider = securityProvider; + } + + @Nonnull + @Override + public ConfigurationParameters getConfigurationParameters() { + return config; + } + + @Override + public List getValidatorProviders() { + ValidatorProvider vp = new UserValidatorProvider(getConfigurationParameters()); + return Collections.singletonList(vp); + } + + @Nonnull + @Override + public List getAuthorizableActions() { + // TODO: create authorizable actions from configuration + return Collections.emptyList(); + } + + @Override + public UserManager getUserManager(Root root, NamePathMapper namePathMapper, Session session) { + return new UserManagerImpl(session, root, namePathMapper, securityProvider); + } + + @Override + public UserManager getUserManager(Root root, NamePathMapper namePathMapper) { + return new UserManagerImpl(null, root, namePathMapper, securityProvider); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImpl.java new file mode 100644 index 00000000000..2404b1ab529 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImpl.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user; + +import java.security.Principal; +import javax.annotation.CheckForNull; +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.api.security.user.Impersonation; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.security.principal.AdminPrincipalImpl; +import org.apache.jackrabbit.oak.spi.security.principal.TreeBasedPrincipal; +import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.apache.jackrabbit.oak.spi.security.user.util.PasswordUtility; +import org.apache.jackrabbit.oak.spi.security.user.util.UserUtility; +import org.apache.jackrabbit.oak.util.NodeUtil; + +import static org.apache.jackrabbit.oak.api.Type.STRING; + +/** + * UserImpl... + */ +class UserImpl extends AuthorizableImpl implements User { + + private final boolean isAdmin; + + UserImpl(String id, Tree tree, UserManagerImpl userManager) throws RepositoryException { + super(id, tree, userManager); + + isAdmin = UserUtility.getAdminId(userManager.getConfig()).equals(id); + } + + //---------------------------------------------------< AuthorizableImpl >--- + @Override + void checkValidTree(Tree tree) throws RepositoryException { + if (tree == null || !UserUtility.isType(tree, AuthorizableType.USER)) { + throw new IllegalArgumentException("Invalid user node: node type rep:User expected."); + } + } + + //-------------------------------------------------------< Authorizable >--- + @Override + public boolean isGroup() { + return false; + } + + @Override + public Principal getPrincipal() throws RepositoryException { + Tree userTree = getTree(); + String principalName = getPrincipalName(); + if (isAdmin()) { + return new AdminPrincipalImpl(principalName, userTree, getUserManager().getNamePathMapper()); + } else { + return new TreeBasedPrincipal(principalName, userTree, getUserManager().getNamePathMapper()); + } + } + + //---------------------------------------------------------------< User >--- + @Override + public boolean isAdmin() { + return isAdmin; + } + + @Override + public Credentials getCredentials() { + return new CredentialsImpl(getID(), getPasswordHash()); + } + + @Override + public Impersonation getImpersonation() throws RepositoryException { + return new ImpersonationImpl(this); + } + + @Override + public void changePassword(String password) throws RepositoryException { + if (password == null) { + throw new RepositoryException("Attempt to set 'null' password for user " + getID()); + } + UserManagerImpl userManager = getUserManager(); + userManager.onPasswordChange(this, password); + userManager.setPassword(getTree(), password, true); + } + + @Override + public void changePassword(String password, String oldPassword) throws RepositoryException { + // make sure the old password matches. + String pwHash = getPasswordHash(); + if (!PasswordUtility.isSame(pwHash, oldPassword)) { + throw new RepositoryException("Failed to change password: Old password does not match."); + } + changePassword(password); + } + + @Override + public void disable(String reason) throws RepositoryException { + if (isAdmin) { + throw new RepositoryException("The administrator user cannot be disabled."); + } + Tree tree = getTree(); + if (reason == null) { + if (tree.hasProperty(REP_DISABLED)) { + // enable the user again. + tree.removeProperty(REP_DISABLED); + } // else: not disabled -> nothing to + } else { + tree.setProperty(REP_DISABLED, reason); + } + } + + @Override + public boolean isDisabled() throws RepositoryException { + return getTree().hasProperty(REP_DISABLED); + } + + @Override + public String getDisabledReason() throws RepositoryException { + PropertyState disabled = getTree().getProperty(REP_DISABLED); + if (disabled != null) { + return disabled.getValue(STRING); + } else { + return null; + } + } + + //------------------------------------------------------------< private >--- + @CheckForNull + private String getPasswordHash() { + NodeUtil n = new NodeUtil(getTree()); + return n.getString(UserConstants.REP_PASSWORD, null); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserManagerImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserManagerImpl.java new file mode 100644 index 00000000000..6bdbee259cd --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserManagerImpl.java @@ -0,0 +1,414 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user; + +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.util.Iterator; +import java.util.List; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.AuthorizableExistsException; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.Query; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.oak.spi.security.principal.PrincipalProvider; +import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType; +import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.apache.jackrabbit.oak.spi.security.user.action.AuthorizableAction; +import org.apache.jackrabbit.oak.spi.security.user.util.PasswordUtility; +import org.apache.jackrabbit.oak.spi.security.user.util.UserUtility; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * UserManagerImpl... + */ +public class UserManagerImpl implements UserManager { + + private static final Logger log = LoggerFactory.getLogger(UserManagerImpl.class); + + private final Session session; + private final Root root; + private final NamePathMapper namePathMapper; + private final SecurityProvider securityProvider; + + private final UserProvider userProvider; + private final MembershipProvider membershipProvider; + private final ConfigurationParameters config; + private final List authorizableActions; + + private UserQueryManager queryManager; + + public UserManagerImpl(Session session, Root root, NamePathMapper namePathMapper, + SecurityProvider securityProvider) { + this.session = session; + this.root = root; + this.namePathMapper = namePathMapper; + this.securityProvider = securityProvider; + + UserConfiguration uc = securityProvider.getUserConfiguration(); + this.config = uc.getConfigurationParameters(); + this.userProvider = new UserProvider(root, config); + this.membershipProvider = new MembershipProvider(root, config); + this.authorizableActions = uc.getAuthorizableActions(); + } + + //--------------------------------------------------------< UserManager >--- + @Override + public Authorizable getAuthorizable(String id) throws RepositoryException { + checkIsLive(); + Authorizable authorizable = null; + Tree tree = userProvider.getAuthorizable(id); + if (tree != null) { + authorizable = getAuthorizable(id, tree); + } + return authorizable; + } + + @Override + public Authorizable getAuthorizable(Principal principal) throws RepositoryException { + checkIsLive(); + return getAuthorizable(userProvider.getAuthorizableByPrincipal(principal)); + } + + @Override + public Authorizable getAuthorizableByPath(String path) throws RepositoryException { + checkIsLive(); + String oakPath = namePathMapper.getOakPath(path); + if (oakPath == null) { + throw new RepositoryException("Invalid path " + path); + } + return getAuthorizable(userProvider.getAuthorizableByPath(oakPath)); + } + + @Override + public Iterator findAuthorizables(String relPath, String value) throws RepositoryException { + return findAuthorizables(relPath, value, SEARCH_TYPE_AUTHORIZABLE); + } + + @Override + public Iterator findAuthorizables(String relPath, String value, int searchType) throws RepositoryException { + checkIsLive(); + return getQueryManager().findAuthorizables(relPath, value, AuthorizableType.getType(searchType)); + } + + @Override + public Iterator findAuthorizables(Query query) throws RepositoryException { + checkIsLive(); + return getQueryManager().find(query); + } + + @Override + public User createUser(final String userID, String password) throws RepositoryException { + Principal principal = new Principal() { + @Override + public String getName() { + return userID; + } + }; + return createUser(userID, password, principal, null); + } + + @Override + public User createUser(String userID, String password, Principal principal, String intermediatePath) throws RepositoryException { + checkIsLive(); + checkValidID(userID); + checkValidPrincipal(principal, false); + + if (intermediatePath != null) { + intermediatePath = namePathMapper.getOakPath(intermediatePath); + } + Tree userTree = userProvider.createUser(userID, intermediatePath); + setPrincipal(userTree, principal); + if (password != null) { + setPassword(userTree, password, true); + } + + User user = new UserImpl(userID, userTree, this); + onCreate(user, password); + + log.debug("User created: " + userID); + return user; + } + + @Override + public Group createGroup(final String groupID) throws RepositoryException { + Principal principal = new Principal() { + @Override + public String getName() { + return groupID; + } + }; + return createGroup(groupID, principal, null); + } + + @Override + public Group createGroup(Principal principal) throws RepositoryException { + return createGroup(principal, null); + } + + @Override + public Group createGroup(Principal principal, String intermediatePath) throws RepositoryException { + return createGroup(principal.getName(), principal, intermediatePath); + } + + @Override + public Group createGroup(String groupID, Principal principal, String intermediatePath) throws RepositoryException { + checkIsLive(); + checkValidID(groupID); + checkValidPrincipal(principal, true); + + if (intermediatePath != null) { + intermediatePath = namePathMapper.getOakPath(intermediatePath); + } + Tree groupTree = userProvider.createGroup(groupID, intermediatePath); + setPrincipal(groupTree, principal); + + Group group = new GroupImpl(groupID, groupTree, this); + onCreate(group); + + log.debug("Group created: " + groupID); + return group; + } + + /** + * Always returns {@code false}. Any modifications made to this user + * manager instance require a subsequent call to {@link javax.jcr.Session#save()} + * in order to have the changes persisted. + * + * @see org.apache.jackrabbit.api.security.user.UserManager#isAutoSave() + */ + @Override + public boolean isAutoSave() { + return false; + } + + /** + * Changing the auto-save behavior is not supported by this implementation + * and this method always throws {@code UnsupportedRepositoryOperationException} + * + * @see UserManager#autoSave(boolean) + */ + @Override + public void autoSave(boolean enable) throws RepositoryException { + throw new UnsupportedRepositoryOperationException("Session#save() is always required."); + } + + + //-------------------------------------------------------------------------- + /** + * Let the configured {@code AuthorizableAction}s perform additional + * tasks associated with the creation of the new user before the + * corresponding new node is persisted. + * + * @param user The new user. + * @param password The password. + * @throws RepositoryException If an exception occurs. + */ + void onCreate(User user, String password) throws RepositoryException { + for (AuthorizableAction action : authorizableActions) { + action.onCreate(user, password, root, namePathMapper); + } + } + + /** + * Let the configured {@code AuthorizableAction}s perform additional + * tasks associated with the creation of the new group before the + * corresponding new node is persisted. + * + * @param group The new group. + * @throws RepositoryException If an exception occurs. + */ + void onCreate(Group group) throws RepositoryException { + for (AuthorizableAction action : authorizableActions) { + action.onCreate(group, root, namePathMapper); + } + } + + /** + * Let the configured {@code AuthorizableAction}s perform any clean + * up tasks related to the authorizable removal (before the corresponding + * node gets removed). + * + * @param authorizable The authorizable to be removed. + * @throws RepositoryException If an exception occurs. + */ + void onRemove(Authorizable authorizable) throws RepositoryException { + for (AuthorizableAction action : authorizableActions) { + action.onRemove(authorizable, root, namePathMapper); + } + } + + /** + * Let the configured {@code AuthorizableAction}s perform additional + * tasks associated with password changing of a given user before the + * corresponding property is being changed. + * + * @param user The target user. + * @param password The new password. + * @throws RepositoryException If an exception occurs. + */ + void onPasswordChange(User user, String password) throws RepositoryException { + for (AuthorizableAction action : authorizableActions) { + action.onPasswordChange(user, password, root, namePathMapper); + } + } + + //-------------------------------------------------------------------------- + @CheckForNull + Node getAuthorizableNode(String id) throws RepositoryException { + if (session == null) { + return null; + } + + Tree tree = userProvider.getAuthorizable(id); + if (tree == null) { + throw new RepositoryException("Authorizable not associated with an existing tree"); + } + String jcrPath = getNamePathMapper().getJcrPath(tree.getPath()); + return session.getNode(jcrPath); + } + + @CheckForNull + Tree getAuthorizableTree(String id) { + Tree tree = userProvider.getAuthorizable(id); + if (tree == null) { + throw new IllegalStateException("Authorizable not associated with an existing tree"); + } + return tree; + } + + @CheckForNull + Authorizable getAuthorizable(Tree tree) throws RepositoryException { + if (tree == null) { + return null; + } + return getAuthorizable(userProvider.getAuthorizableId(tree), tree); + } + + @Nonnull + AuthorizableProperties getAuthorizableProperties(String id) throws RepositoryException { + if (session != null) { + return new JcrAuthorizableProperties(getAuthorizableNode(id), namePathMapper); + } else { + return new OakAuthorizableProperties(userProvider, id, namePathMapper); + } + } + + @Nonnull + NamePathMapper getNamePathMapper() { + return namePathMapper; + } + + @Nonnull + MembershipProvider getMembershipProvider() { + return membershipProvider; + } + + @Nonnull + PrincipalProvider getPrincipalProvider() throws RepositoryException { + return securityProvider.getPrincipalConfiguration().getPrincipalProvider(root, namePathMapper); + } + + @Nonnull + ConfigurationParameters getConfig() { + return config; + } + + @CheckForNull + private Authorizable getAuthorizable(String id, Tree tree) throws RepositoryException { + if (id == null || tree == null) { + return null; + } + if (UserUtility.isType(tree, AuthorizableType.USER)) { + return new UserImpl(userProvider.getAuthorizableId(tree), tree, this); + } else if (UserUtility.isType(tree, AuthorizableType.GROUP)) { + return new GroupImpl(userProvider.getAuthorizableId(tree), tree, this); + } else { + throw new RepositoryException("Not a user or group tree " + tree.getPath() + '.'); + } + } + + private void checkValidID(String ID) throws RepositoryException { + if (ID == null || ID.length() == 0) { + throw new IllegalArgumentException("Invalid ID " + ID); + } else if (getAuthorizable(ID) != null) { + throw new AuthorizableExistsException("Authorizable with ID " + ID + " already exists"); + } + } + + private void checkValidPrincipal(Principal principal, boolean isGroup) { + if (principal == null || principal.getName() == null || "".equals(principal.getName())) { + throw new IllegalArgumentException("Principal may not be null and must have a valid name."); + } + if (!isGroup && EveryonePrincipal.NAME.equals(principal.getName())) { + throw new IllegalArgumentException("'everyone' is a reserved group principal name."); + } + } + + private void setPrincipal(Tree authorizableTree, Principal principal) { + checkNotNull(principal); + authorizableTree.setProperty(UserConstants.REP_PRINCIPAL_NAME, principal.getName()); + } + + void setPassword(Tree userTree, String password, boolean forceHash) throws RepositoryException { + String pwHash; + if (forceHash || PasswordUtility.isPlainTextPassword(password)) { + try { + pwHash = PasswordUtility.buildPasswordHash(password, config); + } catch (NoSuchAlgorithmException e) { + throw new RepositoryException(e); + } catch (UnsupportedEncodingException e) { + throw new RepositoryException(e); + } + } else { + pwHash = password; + } + userTree.setProperty(UserConstants.REP_PASSWORD, pwHash); + } + + private void checkIsLive() throws RepositoryException { + if (session != null && !session.isLive()) { + throw new RepositoryException("UserManager has been closed."); + } + } + + private UserQueryManager getQueryManager() throws RepositoryException { + if (queryManager == null) { + queryManager = new UserQueryManager(this, root); + } + return queryManager; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserProvider.java new file mode 100644 index 00000000000..415efc3bf8f --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserProvider.java @@ -0,0 +1,327 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user; + +import java.security.Principal; +import java.text.ParseException; +import java.util.Collections; +import java.util.Iterator; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.query.Query; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Result; +import org.apache.jackrabbit.oak.api.ResultRow; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.apache.jackrabbit.oak.spi.security.principal.TreeBasedPrincipal; +import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.apache.jackrabbit.oak.spi.security.user.util.UserUtility; +import org.apache.jackrabbit.oak.util.NodeUtil; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.apache.jackrabbit.oak.api.Type.STRING; + +/** + * User provider implementation and manager for group memberships with the + * following characteristics: + * + *

UserProvider

+ * + *

User and Group Creation

+ * This implementation creates the JCR nodes corresponding the a given + * authorizable ID with the following behavior: + *
    + *
  • Users are created below /rep:security/rep:authorizables/rep:users or + * the path configured in the {@link org.apache.jackrabbit.oak.spi.security.user.UserConstants#PARAM_USER_PATH} + * respectively.
  • + *
  • Groups are created below /rep:security/rep:authorizables/rep:groups or + * the path configured in the {@link org.apache.jackrabbit.oak.spi.security.user.UserConstants#PARAM_GROUP_PATH} + * respectively.
  • + *
  • Below each category authorizables are created within a human readable + * structure based on the defined intermediate path or some internal logic + * with a depth defined by the {@code defaultDepth} config option.
    + * E.g. creating a user node for an ID 'aSmith' would result in the following + * structure assuming defaultDepth == 2 is used: + *
    + * + rep:security            [rep:AuthorizableFolder]
    + *   + rep:authorizables     [rep:AuthorizableFolder]
    + *     + rep:users           [rep:AuthorizableFolder]
    + *       + a                 [rep:AuthorizableFolder]
    + *         + aS              [rep:AuthorizableFolder]
    + * ->        + aSmith        [rep:User]
    + * 
    + *
  • + *
  • The node name is calculated from the specified authorizable ID + * {@link org.apache.jackrabbit.util.Text#escapeIllegalJcrChars(String) escaping} any illegal JCR chars.
  • + *
  • If no intermediate path is passed the names of the intermediate + * folders are calculated from the leading chars of the escaped node name.
  • + *
  • If the escaped node name is shorter than the {@code defaultDepth} + * the last char is repeated.
    + * E.g. creating a user node for an ID 'a' would result in the following + * structure assuming defaultDepth == 2 is used: + *
    + * + rep:security            [rep:AuthorizableFolder]
    + *   + rep:authorizables     [rep:AuthorizableFolder]
    + *     + rep:users           [rep:AuthorizableFolder]
    + *       + a                 [rep:AuthorizableFolder]
    + *         + aa              [rep:AuthorizableFolder]
    + * ->        + a             [rep:User]
    + * 
  • + * + *

    Conflicts

    + * + *
      + *
    • If the authorizable node to be created would collide with an existing + * folder the conflict is resolved by using the colling folder as target.
    • + *
    • The current implementation asserts that authorizable nodes are always + * created underneath an node of type {@code rep:AuthorizableFolder}. If this + * condition is violated a {@code ConstraintViolationException} is thrown.
    • + *
    • If the specified intermediate path results in an authorizable node + * being located outside of the configured content structure a + * {@code ConstraintViolationException} is thrown.
    • + *
    + * + *

    Configuration Options

    + *
      + *
    • {@link org.apache.jackrabbit.oak.spi.security.user.UserConstants#PARAM_USER_PATH}: Underneath this structure + * all user nodes are created. Default value is + * "/rep:security/rep:authorizables/rep:users"
    • + *
    • {@link org.apache.jackrabbit.oak.spi.security.user.UserConstants#PARAM_GROUP_PATH}: Underneath this structure + * all group nodes are created. Default value is + * "/rep:security/rep:authorizables/rep:groups"
    • + *
    • {@link org.apache.jackrabbit.oak.spi.security.user.UserConstants#PARAM_DEFAULT_DEPTH}: A positive {@code integer} + * greater than zero defining the depth of the default structure that is + * always created. Default value: 2
    • + *
    + * + *

    Compatibility with Jackrabbit 2.x

    + * + * Due to the fact that this JCR implementation is expected to deal with huge amount + * of child nodes the following configuration options are no longer supported: + *
      + *
    • autoExpandTree
    • + *
    • autoExpandSize
    • + *
    + * + *

    User and Group Access

    + *

    By ID

    + * TODO + *

    By Path

    + * TODO + *

    By Principal Name

    + * TODO + */ +class UserProvider extends AuthorizableBaseProvider { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(UserProvider.class); + + private static final String DELIMITER = "/"; + + private final int defaultDepth; + + private final String groupPath; + private final String userPath; + + UserProvider(Root root, ConfigurationParameters config) { + super(root, config); + + defaultDepth = config.getConfigValue(PARAM_DEFAULT_DEPTH, DEFAULT_DEPTH); + + groupPath = config.getConfigValue(PARAM_GROUP_PATH, DEFAULT_GROUP_PATH); + userPath = config.getConfigValue(PARAM_USER_PATH, DEFAULT_USER_PATH); + } + + @Nonnull + Tree createUser(String userID, String intermediateJcrPath) throws RepositoryException { + return createAuthorizableNode(userID, false, intermediateJcrPath); + } + + @Nonnull + Tree createGroup(String groupID, String intermediateJcrPath) throws RepositoryException { + return createAuthorizableNode(groupID, true, intermediateJcrPath); + } + + @CheckForNull + Tree getAuthorizable(String authorizableId) { + return getByID(authorizableId, AuthorizableType.AUTHORIZABLE); + } + + @CheckForNull + Tree getAuthorizableByPath(String authorizableOakPath) { + return getByPath(authorizableOakPath); + } + + @CheckForNull + Tree getAuthorizableByPrincipal(Principal principal) { + if (principal instanceof TreeBasedPrincipal) { + return root.getTree(((TreeBasedPrincipal) principal).getOakPath()); + } + + // NOTE: in contrast to JR2 the extra shortcut for ID==principalName + // can be omitted as principals names are stored in user defined + // index as well. + try { + StringBuilder stmt = new StringBuilder(); + stmt.append("SELECT * FROM [").append(UserConstants.NT_REP_AUTHORIZABLE).append(']'); + stmt.append("WHERE [").append(UserConstants.REP_PRINCIPAL_NAME).append("] = $principalName"); + Result result = root.getQueryEngine().executeQuery(stmt.toString(), + Query.JCR_SQL2, 1, 0, + Collections.singletonMap("principalName", PropertyValues.newString(principal.getName())), + new NamePathMapper.Default()); + + Iterator rows = result.getRows().iterator(); + if (rows.hasNext()) { + String path = rows.next().getPath(); + return root.getTree(path); + } + } catch (ParseException ex) { + log.error("Failed to retrieve authorizable by principal", ex); + } + + return null; + } + + @CheckForNull + static String getAuthorizableId(Tree authorizableTree) { + checkNotNull(authorizableTree); + if (UserUtility.isType(authorizableTree, AuthorizableType.AUTHORIZABLE)) { + PropertyState idProp = authorizableTree.getProperty(UserConstants.REP_AUTHORIZABLE_ID); + if (idProp != null) { + return idProp.getValue(STRING); + } else { + return Text.unescapeIllegalJcrChars(authorizableTree.getName()); + } + } + return null; + } + + //------------------------------------------------------------< private >--- + + private Tree createAuthorizableNode(String authorizableId, boolean isGroup, String intermediatePath) throws RepositoryException { + String nodeName = Text.escapeIllegalJcrChars(authorizableId); + NodeUtil folder = createFolderNodes(authorizableId, nodeName, isGroup, intermediatePath); + + String ntName = (isGroup) ? NT_REP_GROUP : NT_REP_USER; + NodeUtil authorizableNode = folder.addChild(nodeName, ntName); + + String nodeID = getContentID(authorizableId); + authorizableNode.setString(REP_AUTHORIZABLE_ID, authorizableId); + authorizableNode.setString(JcrConstants.JCR_UUID, nodeID); + + return authorizableNode.getTree(); + } + + /** + * Create folder structure for the authorizable to be created. The structure + * consists of a tree of rep:AuthorizableFolder node(s) starting at the + * configured user or group path. Note that Authorizable nodes are never + * nested. + * + * @param authorizableId The desired authorizable ID. + * @param nodeName The name of the authorizable node. + * @param isGroup Flag indicating whether the new authorizable is a group or a user. + * @param intermediatePath An optional intermediate path. + * @return The folder node. + * @throws RepositoryException If an error occurs + */ + private NodeUtil createFolderNodes(String authorizableId, String nodeName, + boolean isGroup, String intermediatePath) throws RepositoryException { + String authRoot = (isGroup) ? groupPath : userPath; + NodeUtil folder; + Tree authTree = root.getTree(authRoot); + if (authTree == null) { + folder = new NodeUtil(root.getTree("/")); + for (String name : Text.explode(authRoot, '/', false)) { + folder = folder.getOrAddChild(name, NT_REP_AUTHORIZABLE_FOLDER); + } + } else { + folder = new NodeUtil(authTree); + } + + // verification of hierarchy and node types is delegated to UserValidator upon commit + String folderPath = getFolderPath(authorizableId, intermediatePath, authRoot); + String[] segmts = Text.explode(folderPath, '/', false); + for (String segment : segmts) { + if (".".equals(segment)) { + // nothing to do + } else if ("..".equals(segment)) { + folder = folder.getParent(); + } else { + folder = folder.getOrAddChild(segment, NT_REP_AUTHORIZABLE_FOLDER); + } + } + + // test for colliding folder child node. + while (folder.hasChild(nodeName)) { + NodeUtil colliding = folder.getChild(nodeName); + if (colliding.hasPrimaryNodeTypeName(NT_REP_AUTHORIZABLE_FOLDER)) { + log.debug("Existing folder node collides with user/group to be created. Expanding path by: " + colliding.getName()); + folder = colliding; + } else { + String msg = "Failed to create authorizable with id '" + authorizableId + "' : " + + "Detected conflicting node of unexpected node type '" + colliding.getString(JcrConstants.JCR_PRIMARYTYPE, null) + "'."; + log.error(msg); + throw new ConstraintViolationException(msg); + } + } + + return folder; + } + + private String getFolderPath(String authorizableId, String intermediatePath, String authRoot) throws ConstraintViolationException { + if (intermediatePath != null && intermediatePath.charAt(0) == '/') { + if (!intermediatePath.startsWith(authRoot)) { + throw new ConstraintViolationException("Attempt to create authorizable outside of configured tree"); + } else { + intermediatePath = intermediatePath.substring(authRoot.length()+1); + } + } + + StringBuilder sb = new StringBuilder(); + if (intermediatePath != null && !intermediatePath.isEmpty()) { + sb.append(intermediatePath); + } else { + int idLength = authorizableId.length(); + StringBuilder segment = new StringBuilder(); + for (int i = 0; i < defaultDepth; i++) { + if (idLength > i) { + segment.append(authorizableId.charAt(i)); + } else { + // escapedID is too short -> append the last char again + segment.append(authorizableId.charAt(idLength-1)); + } + sb.append(DELIMITER).append(Text.escapeIllegalJcrChars(segment.toString())); + } + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserQueryManager.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserQueryManager.java new file mode 100644 index 00000000000..f505ccc8616 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserQueryManager.java @@ -0,0 +1,232 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user; + +import java.text.ParseException; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import javax.annotation.Nonnull; +import javax.jcr.RepositoryException; + +import com.google.common.base.Function; +import com.google.common.base.Predicates; +import com.google.common.collect.Iterators; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Query; +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Result; +import org.apache.jackrabbit.oak.api.ResultRow; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.SessionQueryEngine; +import org.apache.jackrabbit.oak.security.user.query.XPathQueryBuilder; +import org.apache.jackrabbit.oak.security.user.query.XPathQueryEvaluator; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; +import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.apache.jackrabbit.util.ISO9075; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * UserQueryManager... TODO + */ +class UserQueryManager { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(UserQueryManager.class); + + private final UserManagerImpl userManager; + private final Root root; + + private final String userRoot; + private final String groupRoot; + private final String authorizableRoot; + + UserQueryManager(UserManagerImpl userManager, Root root) throws RepositoryException { + this.userManager = userManager; + this.root = root; + + this.userRoot = userManager.getConfig().getConfigValue(UserConstants.PARAM_USER_PATH, UserConstants.DEFAULT_USER_PATH); + this.groupRoot = userManager.getConfig().getConfigValue(UserConstants.PARAM_GROUP_PATH, UserConstants.DEFAULT_GROUP_PATH); + + String parent = userRoot; + while (!Text.isDescendant(parent, groupRoot)) { + parent = Text.getRelativeParent(parent, 1); + } + authorizableRoot = parent; + } + + @Nonnull + Iterator find(Query query) throws RepositoryException { + XPathQueryBuilder builder = new XPathQueryBuilder(); + query.build(builder); + return new XPathQueryEvaluator(builder, userManager, root, userManager.getNamePathMapper()).eval(); + } + + @Nonnull + Iterator findAuthorizables(String relativePath, String value, + AuthorizableType authorizableType) + throws RepositoryException { + String oakPath = userManager.getNamePathMapper().getOakPath(relativePath); + return findAuthorizables(oakPath, value, true, authorizableType); + } + + /** + * Find the authorizable trees matching the following search parameters within + * the sub-tree defined by an authorizable tree: + * + * @param relPath A relative path (or a name) pointing to properties within + * the tree defined by a given authorizable node. + * @param value The property value to look for. + * @param exact A boolean flag indicating if the value must match exactly or not.s + * @param type Filter the search results to only return authorizable + * trees of a given type. Passing {@link org.apache.jackrabbit.oak.spi.security.user.AuthorizableType#AUTHORIZABLE} indicates that + * no filtering for a specific authorizable type is desired. However, properties + * might still be search in the complete sub-tree of authorizables depending + * on the other query parameters. + * @return An iterator of authorizable trees that match the specified + * search parameters and filters or an empty iterator if no result can be + * found. + * @throws javax.jcr.RepositoryException If an error occurs. + */ + @Nonnull + Iterator findAuthorizables(String relPath, String value, + boolean exact, AuthorizableType type) throws RepositoryException { + String statement = buildXPathStatement(relPath, value, exact, type); + SessionQueryEngine queryEngine = root.getQueryEngine(); + try { + Map bindings = (value != null) ? Collections.singletonMap("propValue", PropertyValues.newString(value)) : null; + Result result = queryEngine.executeQuery(statement, javax.jcr.query.Query.XPATH, Long.MAX_VALUE, 0, bindings, userManager.getNamePathMapper()); + return Iterators.filter(Iterators.transform(result.getRows().iterator(), new ResultRowToAuthorizable()), Predicates.notNull()); + } catch (ParseException e) { + throw new RepositoryException(e); + } + } + + //------------------------------------------------------------< private >--- + @Nonnull + private String buildXPathStatement(String relPath, String value, boolean exact, AuthorizableType type) { + StringBuilder stmt = new StringBuilder(); + String searchRoot = getSearchRoot(type); + if (!"/".equals(searchRoot)) { + stmt.append(searchRoot); + } + + String path; + String propName; + String ntName; + if (relPath.indexOf('/') == -1) { + // search for properties somewhere below an authorizable node + path = null; + propName = relPath; + ntName = null; + } else { + // FIXME: proper normalization of the relative path + path = (relPath.startsWith("./") ? null : Text.getRelativeParent(relPath, 1)); + propName = Text.getName(relPath); + ntName = getNodeTypeName(type); + } + + stmt.append("//"); + if (path != null) { + stmt.append(path); + } else { + if (ntName != null) { + stmt.append("element(*,"); + stmt.append(ntName); + } else { + stmt.append("element(*"); + } + stmt.append(')'); + } + + if (value != null) { + stmt.append('['); + stmt.append((exact) ? "@" : "jcr:like(@"); + stmt.append(ISO9075.encode(propName)); + if (exact) { + stmt.append("='"); + stmt.append(value.replaceAll("'", "''")); + stmt.append('\''); + } else { + stmt.append(",'%"); + stmt.append(escapeForQuery(value)); + stmt.append("%')"); + } + stmt.append(']'); + } + return stmt.toString(); + } + + /** + * @param type + * @return The path of search root for the specified authorizable type. + */ + @Nonnull + private String getSearchRoot(AuthorizableType type) { + if (type == AuthorizableType.USER) { + return userRoot; + } else if (type == AuthorizableType.GROUP) { + return groupRoot; + } else { + return authorizableRoot; + } + } + + @Nonnull + private static String escapeForQuery(String value) { + StringBuilder ret = new StringBuilder(); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '\\') { + ret.append("\\\\"); + } else if (c == '\'') { + ret.append("''"); + } else { + ret.append(c); + } + } + return ret.toString(); + } + + @Nonnull + private static String getNodeTypeName(AuthorizableType type) { + if (type == AuthorizableType.USER) { + return UserConstants.NT_REP_USER; + } else if (type == AuthorizableType.GROUP) { + return UserConstants.NT_REP_GROUP; + } else { + return UserConstants.NT_REP_AUTHORIZABLE; + } + } + + private class ResultRowToAuthorizable implements Function { + @Override + public Authorizable apply(ResultRow row) { + try { + return userManager.getAuthorizable(row.getPath()); + } catch (RepositoryException e) { + log.debug("Failed to access authorizable " + row.getPath()); + return null; + } + } + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserValidator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserValidator.java new file mode 100644 index 00000000000..c84b0dd5494 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserValidator.java @@ -0,0 +1,198 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user; + +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.spi.commit.DefaultValidator; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType; +import org.apache.jackrabbit.oak.spi.security.user.util.PasswordUtility; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.apache.jackrabbit.oak.spi.security.user.util.UserUtility; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.util.NodeUtil; +import org.apache.jackrabbit.util.Text; + +/** + * Validator that enforces user management specific constraints. Please note that + * is this validator is making implementation specific assumptions; if the + * user management implementation is replace it is most probably necessary to + * provide a custom validator as well. + */ +class UserValidator extends DefaultValidator implements UserConstants { + + private final UserValidatorProvider provider; + + private final NodeUtil parentBefore; + private final NodeUtil parentAfter; + + UserValidator(NodeUtil parentBefore, NodeUtil parentAfter, UserValidatorProvider provider) { + this.parentBefore = parentBefore; + this.parentAfter = parentAfter; + + this.provider = provider; + } + + //----------------------------------------------------------< Validator >--- + + @Override + public void propertyAdded(PropertyState after) throws CommitFailedException { + if (!isAuthorizable(parentAfter)) { + return; + } + + String name = after.getName(); + if (REP_DISABLED.equals(name) && isAdminUser(parentAfter)) { + String msg = "Admin user cannot be disabled."; + fail(msg); + } + + if (JcrConstants.JCR_UUID.equals(name) && !isValidUUID(after.getValue(Type.STRING))) { + String msg = "Invalid jcr:uuid for authorizable " + parentAfter.getName(); + fail(msg); + } + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) throws CommitFailedException { + if (!isAuthorizable(parentAfter)) { + return; + } + + String name = before.getName(); + if (REP_PRINCIPAL_NAME.equals(name) || REP_AUTHORIZABLE_ID.equals(name)) { + String msg = "Authorizable property " + name + " may not be altered after user/group creation."; + fail(msg); + } else if (JcrConstants.JCR_UUID.equals(name) && !isValidUUID(after.getValue(Type.STRING))) { + String msg = "Invalid jcr:uuid for authorizable " + parentAfter.getName(); + fail(msg); + } + + if (isUser(parentBefore) && REP_PASSWORD.equals(name) && PasswordUtility.isPlainTextPassword(after.getValue(Type.STRING))) { + String msg = "Password may not be plain text."; + fail(msg); + } + } + + + @Override + public void propertyDeleted(PropertyState before) throws CommitFailedException { + if (!isAuthorizable(parentAfter)) { + return; + } + + String name = before.getName(); + if (REP_PASSWORD.equals(name) || REP_PRINCIPAL_NAME.equals(name) || REP_AUTHORIZABLE_ID.equals(name)) { + String msg = "Authorizable property " + name + " may not be removed."; + fail(msg); + } + } + + @Override + public Validator childNodeAdded(String name, NodeState after) throws CommitFailedException { + NodeUtil node = parentAfter.getChild(name); + String authRoot = null; + if (node.hasPrimaryNodeTypeName(NT_REP_USER)) { + authRoot = provider.getConfig().getConfigValue(PARAM_USER_PATH, DEFAULT_USER_PATH); + } else if (node.hasPrimaryNodeTypeName(UserConstants.NT_REP_GROUP)) { + authRoot = provider.getConfig().getConfigValue(PARAM_GROUP_PATH, DEFAULT_GROUP_PATH); + } + if (authRoot != null) { + assertHierarchy(node, authRoot); + // assert rep:principalName is present (that should actually by covered + // by node type validator) + if (node.getString(REP_PRINCIPAL_NAME, null) == null) { + fail("Mandatory property rep:principalName missing."); + } + } + return new UserValidator(null, node, provider); + } + + @Override + public Validator childNodeChanged(String name, NodeState before, NodeState after) throws CommitFailedException { + // TODO: anything to do here? + return new UserValidator(parentBefore.getChild(name), parentAfter.getChild(name), provider); + } + + @Override + public Validator childNodeDeleted(String name, NodeState before) throws CommitFailedException { + NodeUtil node = parentBefore.getChild(name); + if (isAdminUser(node)) { + String msg = "The admin user cannot be removed."; + fail(msg); + } + return null; + } + + //------------------------------------------------------------< private >--- + + /** + * Make sure user and group nodes are located underneath the configured path + * and that path consists of rep:authorizableFolder nodes. + * + * @param userNode + * @param pathConstraint + * @throws CommitFailedException + */ + private void assertHierarchy(NodeUtil userNode, String pathConstraint) throws CommitFailedException { + if (!Text.isDescendant(pathConstraint, userNode.getTree().getPath())) { + String msg = "Attempt to create user/group outside of configured scope " + pathConstraint; + fail(msg); + } + + NodeUtil parent = userNode.getParent(); + while (!parent.getTree().isRoot()) { + if (!parent.hasPrimaryNodeTypeName(NT_REP_AUTHORIZABLE_FOLDER)) { + String msg = "Cannot create user/group: Intermediate folders must be of type rep:AuthorizableFolder."; + fail(msg); + } + parent = parent.getParent(); + } + } + + + // FIXME: copied from UserProvider#isAdminUser + private boolean isAdminUser(NodeUtil userNode) { + String id = (userNode.getString(REP_AUTHORIZABLE_ID, Text.unescapeIllegalJcrChars(userNode.getName()))); + return isUser(userNode) && UserUtility.getAdminId(provider.getConfig()).equals(id); + } + + private boolean isValidUUID(String uuid) { + String id = UserProvider.getAuthorizableId(parentAfter.getTree()); + return uuid.equals(UserProvider.getContentID(id)); + } + + private static boolean isAuthorizable(NodeUtil node) { + return UserUtility.isType(node.getTree(), AuthorizableType.AUTHORIZABLE); + } + + private static boolean isUser(NodeUtil node) { + return UserUtility.isType(node.getTree(), AuthorizableType.USER); + } + + + + private static void fail(String msg) throws CommitFailedException { + Exception e = new ConstraintViolationException(msg); + throw new CommitFailedException(e); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserValidatorProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserValidatorProvider.java new file mode 100644 index 00000000000..61d84d2684a --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserValidatorProvider.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.security.user; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.core.ReadOnlyTree; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.util.NodeUtil; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Provides a validator for user and group management. + */ +class UserValidatorProvider implements ValidatorProvider { + + private final ConfigurationParameters config; + + UserValidatorProvider(ConfigurationParameters config) { + this.config = checkNotNull(config); + } + + //--------------------------------------------------< ValidatorProvider >--- + @Nonnull + @Override + public Validator getRootValidator(NodeState before, NodeState after) { + + NodeUtil rootBefore = new NodeUtil(new ReadOnlyTree(before)); + NodeUtil rootAfter = new NodeUtil(new ReadOnlyTree(after)); + + return new UserValidator(rootBefore, rootAfter, this); + } + + //-----------------------------------------------------------< internal >--- + @Nonnull + ConfigurationParameters getConfig() { + return config; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/query/Condition.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/query/Condition.java new file mode 100644 index 00000000000..4203cd07995 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/query/Condition.java @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user.query; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + + +interface Condition { + + void accept(ConditionVisitor visitor) throws RepositoryException; + + //------------------------------------------< Condition implementations >--- + + static class Node implements Condition { + private final String pattern; + + public Node(String pattern) { + this.pattern = pattern; + } + + public String getPattern() { + return pattern; + } + + public void accept(ConditionVisitor visitor) throws RepositoryException { + visitor.visit(this); + } + } + + static class Property implements Condition { + private final String relPath; + private final RelationOp op; + private final Value value; + private final String pattern; + + public Property(String relPath, RelationOp op, Value value) { + this.relPath = relPath; + this.op = op; + this.value = value; + pattern = null; + } + + public Property(String relPath, RelationOp op, String pattern) { + this.relPath = relPath; + this.op = op; + value = null; + this.pattern = pattern; + } + + public Property(String relPath, RelationOp op) { + this.relPath = relPath; + this.op = op; + value = null; + pattern = null; + } + + public String getRelPath() { + return relPath; + } + + public RelationOp getOp() { + return op; + } + + public Value getValue() { + return value; + } + + public String getPattern() { + return pattern; + } + + public void accept(ConditionVisitor visitor) throws RepositoryException { + visitor.visit(this); + } + } + + static class Contains implements Condition { + private final String relPath; + private final String searchExpr; + + public Contains(String relPath, String searchExpr) { + this.relPath = relPath; + this.searchExpr = searchExpr; + } + + public String getRelPath() { + return relPath; + } + + public String getSearchExpr() { + return searchExpr; + } + + public void accept(ConditionVisitor visitor) { + visitor.visit(this); + } + } + + static class Impersonation implements Condition { + private final String name; + + public Impersonation(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void accept(ConditionVisitor visitor) { + visitor.visit(this); + } + } + + static class Not implements Condition { + private final Condition condition; + + public Not(Condition condition) { + this.condition = condition; + } + + public Condition getCondition() { + return condition; + } + + public void accept(ConditionVisitor visitor) throws RepositoryException { + visitor.visit(this); + } + } + + abstract static class Compound implements Condition, Iterable { + private final List conditions = new ArrayList(); + + public Compound() { + super(); + } + + public Compound(Condition condition1, Condition condition2) { + conditions.add(condition1); + conditions.add(condition2); + } + + public void addCondition(Condition condition) { + conditions.add(condition); + } + + public Iterator iterator() { + return conditions.iterator(); + } + } + + static class And extends Compound { + public And(Condition condition1, Condition condition2) { + super(condition1, condition2); + } + + public void accept(ConditionVisitor visitor) throws RepositoryException { + visitor.visit(this); + } + } + + static class Or extends Compound { + public Or(Condition condition1, Condition condition2) { + super(condition1, condition2); + } + + public void accept(ConditionVisitor visitor) throws RepositoryException { + visitor.visit(this); + } + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/query/ConditionVisitor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/query/ConditionVisitor.java new file mode 100644 index 00000000000..9f6267ce657 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/query/ConditionVisitor.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user.query; + +import javax.jcr.RepositoryException; + +interface ConditionVisitor { + + void visit(Condition.Node node) throws RepositoryException; + + void visit(Condition.Property condition) throws RepositoryException; + + void visit(Condition.Contains condition); + + void visit(Condition.Impersonation condition); + + void visit(Condition.Not condition) throws RepositoryException; + + void visit(Condition.And condition) throws RepositoryException; + + void visit(Condition.Or condition) throws RepositoryException; +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/query/RelationOp.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/query/RelationOp.java new file mode 100644 index 00000000000..19a639eddfc --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/query/RelationOp.java @@ -0,0 +1,28 @@ +package org.apache.jackrabbit.oak.security.user.query; + +/** + * Relational operators for comparing a property to a value. Correspond + * to the general comparison operators as define in JSR-170. + * The {@link #EX} tests for existence of a property. + */ +enum RelationOp { + + NE("!="), + EQ("="), + LT("<"), + LE("<="), + GT(">"), + GE("=>"), + EX(""), + LIKE("like"); + + private final String op; + + RelationOp(String op) { + this.op = op; + } + + String getOp() { + return op; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/query/ResultIterator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/query/ResultIterator.java new file mode 100644 index 00000000000..fe169501b4c --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/query/ResultIterator.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user.query; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * Implements a query result iterator which only returns a maximum number of + * element from an underlying iterator starting at a given offset. + * + * @param element type of the query results + * + * TODO move to query-commons ? + */ +public class ResultIterator implements Iterator { + + public final static int OFFSET_NONE = 0; + public final static int MAX_ALL = -1; + + private final Iterator iterator; + private final long offset; + private final long max; + private int pos; + private T next; + + /** + * Create a new {@code ResultIterator} with a given offset and maximum + * + * @param offset Offset to start iteration at. Must be non negative + * @param max Maximum elements this iterator should return. + * Set to {@link #MAX_ALL} for all results. + * @param iterator the underlying iterator + * @throws IllegalArgumentException if offset is negative + */ + private ResultIterator(long offset, long max, Iterator iterator) { + if (offset < OFFSET_NONE) { + throw new IllegalArgumentException("Offset must not be negative"); + } + this.iterator = iterator; + this.offset = offset; + this.max = max; + } + + /** + * Returns an iterator respecting the specified {@code offset} and {@code max}. + * + * @param offset offset to start iteration at. Must be non negative + * @param max maximum elements this iterator should return. Set to + * {@link #MAX_ALL} for all + * @param iterator the underlying iterator + * @param element type + * @return an iterator which only returns the elements in the given bounds + */ + public static Iterator create(long offset, long max, Iterator iterator) { + if (offset == OFFSET_NONE && max == MAX_ALL) { + // no constraints on offset nor max -> return the original iterator. + return iterator; + } else { + return new ResultIterator(offset, max, iterator); + } + } + + //-----------------------------------------------------------< Iterator >--- + @Override + public boolean hasNext() { + if (next == null) { + fetchNext(); + } + return next != null; + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return consumeNext(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + //------------------------------------------------------------< private >--- + + private void fetchNext() { + for (; pos < offset && iterator.hasNext(); pos++) { + next = iterator.next(); + } + + if (pos < offset || !iterator.hasNext() || max >= 0 && pos - offset + 1 > max) { + next = null; + } else { + next = iterator.next(); + pos++; + } + } + + private T consumeNext() { + T element = next; + next = null; + return element; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/query/XPathQueryBuilder.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/query/XPathQueryBuilder.java new file mode 100644 index 00000000000..c273b8b7dd0 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/query/XPathQueryBuilder.java @@ -0,0 +1,195 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user.query; + +import javax.jcr.Value; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.QueryBuilder; + +public class XPathQueryBuilder implements QueryBuilder { + + private Class selector = Authorizable.class; + private String groupName; + private boolean declaredMembersOnly; + private Condition condition; + private String sortProperty; + private Direction sortDirection = Direction.ASCENDING; + private boolean sortIgnoreCase; + private Value bound; + private long offset; + private long maxCount = -1; + + //-------------------------------------------------------< QueryBuilder >--- + @Override + public void setSelector(Class selector) { + this.selector = selector; + } + + @Override + public void setScope(String groupName, boolean declaredOnly) { + this.groupName = groupName; + declaredMembersOnly = declaredOnly; + } + + @Override + public void setCondition(Condition condition) { + this.condition = condition; + } + + @Override + public void setSortOrder(String propertyName, Direction direction, boolean ignoreCase) { + sortProperty = propertyName; + sortDirection = direction; + sortIgnoreCase = ignoreCase; + } + + @Override + public void setSortOrder(String propertyName, Direction direction) { + setSortOrder(propertyName, direction, false); + } + + @Override + public void setLimit(Value bound, long maxCount) { + offset = 0; // Unset any previously set offset + this.bound = bound; + this.maxCount = maxCount; + } + + @Override + public void setLimit(long offset, long maxCount) { + bound = null; // Unset any previously set bound + this.offset = offset; + this.maxCount = maxCount; + } + + @Override + public Condition nameMatches(String pattern) { + return new Condition.Node(pattern); + } + + @Override + public Condition neq(String relPath, Value value) { + return new Condition.Property(relPath, RelationOp.NE, value); + } + + @Override + public Condition eq(String relPath, Value value) { + return new Condition.Property(relPath, RelationOp.EQ, value); + } + + @Override + public Condition lt(String relPath, Value value) { + return new Condition.Property(relPath, RelationOp.LT, value); + } + + @Override + public Condition le(String relPath, Value value) { + return new Condition.Property(relPath, RelationOp.LE, value); + } + + @Override + public Condition gt(String relPath, Value value) { + return new Condition.Property(relPath, RelationOp.GT, value); + } + + @Override + public Condition ge(String relPath, Value value) { + return new Condition.Property(relPath, RelationOp.GE, value); + } + + @Override + public Condition exists(String relPath) { + return new Condition.Property(relPath, RelationOp.EX); + } + + @Override + public Condition like(String relPath, String pattern) { + return new Condition.Property(relPath, RelationOp.LIKE, pattern); + } + + @Override + public Condition contains(String relPath, String searchExpr) { + return new Condition.Contains(relPath, searchExpr); + } + + @Override + public Condition impersonates(String name) { + return new Condition.Impersonation(name); + } + + @Override + public Condition not(Condition condition) { + return new Condition.Not(condition); + } + + @Override + public Condition and(Condition condition1, Condition condition2) { + return new Condition.And(condition1, condition2); + } + + @Override + public Condition or(Condition condition1, Condition condition2) { + return new Condition.Or(condition1, condition2); + } + + //-----------------------------------------------------------< internal >--- + + Condition property(String relPath, RelationOp op, Value value) { + return new Condition.Property(relPath, op, value); + } + + Class getSelector() { + return selector; + } + + String getGroupName() { + return groupName; + } + + boolean isDeclaredMembersOnly() { + return declaredMembersOnly; + } + + Condition getCondition() { + return condition; + } + + String getSortProperty() { + return sortProperty; + } + + Direction getSortDirection() { + return sortDirection; + } + + boolean getSortIgnoreCase() { + return sortIgnoreCase; + } + + Value getBound() { + return bound; + } + + long getOffset() { + return offset; + } + + long getMaxCount() { + return maxCount; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/query/XPathQueryEvaluator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/query/XPathQueryEvaluator.java new file mode 100644 index 00000000000..10257aa962d --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/query/XPathQueryEvaluator.java @@ -0,0 +1,337 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user.query; + +import java.text.ParseException; +import java.util.Iterator; +import javax.annotation.Nonnull; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.query.Query; + +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.Iterators; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.QueryBuilder; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.api.ResultRow; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This evaluator for {@link org.apache.jackrabbit.api.security.user.Query}s use XPath + * and some minimal client side filtering. + */ +public class XPathQueryEvaluator implements ConditionVisitor { + static final Logger log = LoggerFactory.getLogger(XPathQueryEvaluator.class); + + private final XPathQueryBuilder builder; + private final UserManager userManager; + private final Root root; + private final NamePathMapper namePathMapper; + + private final StringBuilder xPath = new StringBuilder(); + + public XPathQueryEvaluator(XPathQueryBuilder builder, UserManager userManager, + Root root, NamePathMapper namePathMapper) { + this.builder = builder; + this.userManager = userManager; + this.root = root; + this.namePathMapper = namePathMapper; + } + + public Iterator eval() throws RepositoryException { + // shortcut + if (builder.getMaxCount() == 0) { + return Iterators.emptyIterator(); + } + + xPath.append("//element(*,") + .append(getNtName(builder.getSelector())) + .append(')'); + + Value bound = builder.getBound(); + + Condition condition = builder.getCondition(); + String sortCol = builder.getSortProperty(); + QueryBuilder.Direction sortDir = builder.getSortDirection(); + if (bound != null) { + if (sortCol == null) { + log.warn("Ignoring bound {} since no sort order is specified"); + } else { + Condition boundCondition = builder.property(sortCol, getCollation(sortDir), bound); + condition = condition == null + ? boundCondition + : builder.and(condition, boundCondition); + } + } + + if (condition != null) { + xPath.append('['); + condition.accept(this); + xPath.append(']'); + } + + if (sortCol != null) { + boolean ignoreCase = builder.getSortIgnoreCase(); + xPath.append(" order by ") + .append(ignoreCase ? "" : "fn:lower-case(") + .append(sortCol) + .append(ignoreCase ? " " : ") ") + .append(sortDir.getDirection()); + } + + try { + if (builder.getGroupName() == null) { + long offset = builder.getOffset(); + if (bound != null && offset > 0) { + log.warn("Found bound {} and offset {} in limit. Discarding offset.", bound, offset); + offset = 0; + } + return findAuthorizables(builder.getMaxCount(), offset); + } else { + // filtering by group name not included in query -> enforce offset + // and limit on the result set. + Iterator result = findAuthorizables(Long.MAX_VALUE, 0); + Iterator filtered = filter(result, builder.getGroupName(), builder.isDeclaredMembersOnly()); + return ResultIterator.create(builder.getOffset(), builder.getMaxCount(), filtered); + } + } catch (ParseException e) { + throw new RepositoryException(e); + } + + // If we are scoped to a group and have a limit, we have to apply the limit + // here (inefficient!) otherwise we can apply the limit in the query + + } + + //---------------------------------------------------< ConditionVisitor >--- + @Override + public void visit(Condition.Node condition) throws RepositoryException { + xPath.append('(') + .append("jcr:like(@") + .append(namePathMapper.getJcrName(UserConstants.REP_PRINCIPAL_NAME)) + .append(",'") + .append(condition.getPattern()) + .append("')") + .append(" or ") + .append("jcr:like(fn:name(),'") + .append(escape(condition.getPattern())) + .append("')") + .append(')'); + } + + @Override + public void visit(Condition.Property condition) throws RepositoryException { + RelationOp relOp = condition.getOp(); + if (relOp == RelationOp.EX) { + xPath.append(condition.getRelPath()); + } else if (relOp == RelationOp.LIKE) { + xPath.append("jcr:like(") + .append(condition.getRelPath()) + .append(",'") + .append(condition.getPattern()) + .append("')"); + } else { + xPath.append(condition.getRelPath()) + .append(condition.getOp().getOp()) + .append(format(condition.getValue())); + } + } + + @Override + public void visit(Condition.Contains condition) { + xPath.append("jcr:contains(") + .append(condition.getRelPath()) + .append(",'") + .append(condition.getSearchExpr()) + .append("')"); + } + + @Override + public void visit(Condition.Impersonation condition) { + xPath.append("@rep:impersonators='") + .append(condition.getName()) + .append('\''); + } + + @Override + public void visit(Condition.Not condition) throws RepositoryException { + xPath.append("not("); + condition.getCondition().accept(this); + xPath.append(')'); + } + + @Override + public void visit(Condition.And condition) throws RepositoryException { + int count = 0; + for (Condition c : condition) { + xPath.append(count++ > 0 ? " and " : ""); + c.accept(this); + } + } + + @Override + public void visit(Condition.Or condition) throws RepositoryException { + int pos = xPath.length(); + + int count = 0; + for (Condition c : condition) { + xPath.append(count++ > 0 ? " or " : ""); + c.accept(this); + } + + // Surround or clause with parentheses if it contains more than one term + if (count > 1) { + xPath.insert(pos, '('); + xPath.append(')'); + } + } + + //------------------------------------------------------------< private >--- + /** + * Escape {@code string} for matching in jcr escaped node names + * + * @param string string to escape + * @return escaped string + */ + @Nonnull + public static String escape(String string) { + StringBuilder result = new StringBuilder(); + + int k = 0; + int j; + do { + j = string.indexOf('%', k); // split on % + if (j < 0) { + // jcr escape trail + result.append(Text.escapeIllegalJcrChars(string.substring(k))); + } else if (j > 0 && string.charAt(j - 1) == '\\') { + // literal occurrence of % -> jcr escape + result.append(Text.escapeIllegalJcrChars(string.substring(k, j) + '%')); + } else { + // wildcard occurrence of % -> jcr escape all but % + result.append(Text.escapeIllegalJcrChars(string.substring(k, j))).append('%'); + } + + k = j + 1; + } while (j >= 0); + + return result.toString(); + } + + @Nonnull + private String getNtName(Class selector) { + String ntName; + if (User.class.isAssignableFrom(selector)) { + ntName = namePathMapper.getJcrName(UserConstants.NT_REP_USER); + } else if (Group.class.isAssignableFrom(selector)) { + ntName = namePathMapper.getJcrName(UserConstants.NT_REP_GROUP); + } else { + ntName = namePathMapper.getJcrName(UserConstants.NT_REP_AUTHORIZABLE); + } + if (ntName == null) { + log.warn("Failed to retrieve JCR name for authorizable node type."); + ntName = UserConstants.NT_REP_AUTHORIZABLE; + } + return ntName; + } + + @Nonnull + private static String format(Value value) throws RepositoryException { + switch (value.getType()) { + case PropertyType.STRING: + case PropertyType.BOOLEAN: + return '\'' + value.getString() + '\''; + + case PropertyType.LONG: + case PropertyType.DOUBLE: + return value.getString(); + + case PropertyType.DATE: + return "xs:dateTime('" + value.getString() + "')"; + + default: + throw new RepositoryException("Property of type " + PropertyType.nameFromValue(value.getType()) + + " not supported"); + } + } + + @Nonnull + private static RelationOp getCollation(QueryBuilder.Direction direction) throws RepositoryException { + switch (direction) { + case ASCENDING: + return RelationOp.GT; + case DESCENDING: + return RelationOp.LT; + default: + throw new RepositoryException("Unknown sort order " + direction); + } + } + + @Nonnull + private Iterator findAuthorizables(long limit, long offset) throws ParseException { + Iterable resultRows = root.getQueryEngine().executeQuery(xPath.toString(), Query.XPATH, limit, offset, null, namePathMapper).getRows(); + + Function transformer = new Function() { + public Authorizable apply(ResultRow resultRow) { + try { + return userManager.getAuthorizableByPath(resultRow.getPath()); + } catch (RepositoryException e) { + log.warn("Cannot create authorizable from result row {}", resultRow); + log.debug(e.getMessage(), e); + return null; + } + } + }; + + return Iterators.filter(Iterators.transform(resultRows.iterator(), transformer), Predicates.notNull()); + } + + @Nonnull + private Iterator filter(Iterator authorizables, + String groupName, + final boolean declaredMembersOnly) throws RepositoryException { + Predicate predicate; + Authorizable authorizable = userManager.getAuthorizable(groupName); + if (authorizable == null || !authorizable.isGroup()) { + predicate = Predicates.alwaysFalse(); + } else { + final Group group = (Group) authorizable; + predicate = new Predicate() { + public boolean apply(Authorizable authorizable) { + try { + return (declaredMembersOnly) ? group.isDeclaredMember(authorizable) : group.isMember(authorizable); + } catch (RepositoryException e) { + log.debug("Cannot determine group membership for {}", authorizable, e.getMessage()); + return false; + } + } + }; + } + return Iterators.filter(authorizables, predicate); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/CommitHook.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/CommitHook.java new file mode 100644 index 00000000000..4fc60e06b90 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/CommitHook.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.spi.commit; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * Extension point for validating and modifying content changes. Available + * commit hooks are called in sequence to process incoming content changes + * before they get persisted and shared with other clients. + *

    + * A commit hook can throw a {@link CommitFailedException} for a particular + * change to prevent it from being persisted, or it can modify the changes + * for example to update an in-content index or to add auto-generated content. + *

    + * Note that instead of implementing this interface directly, most commit + * editors and validators are better expressed as implementations of the + * more specific extension interfaces defined in this package. + */ +public interface CommitHook { + + /** + * Validates and/or modifies the given content change before it gets + * persisted. + * + * @param before content tree before the commit + * @param after content tree prepared for the commit + * @return content tree to be committed + * @throws CommitFailedException if the commit should be rejected + */ + @Nonnull + NodeState processCommit(NodeState before, NodeState after) + throws CommitFailedException; + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/CompositeHook.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/CompositeHook.java new file mode 100644 index 00000000000..81f105666eb --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/CompositeHook.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.commit; + +import java.util.Arrays; +import java.util.Collection; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * Composite commit hook. Maintains a list of component hooks and takes + * care of calling them in proper sequence. + */ +public class CompositeHook implements CommitHook { + + public static CommitHook compose(@Nonnull Collection hooks) { + switch (hooks.size()) { + case 0: + return EmptyHook.INSTANCE; + case 1: + return hooks.iterator().next(); + default: + return new CompositeHook(hooks); + } + } + + private final Collection hooks; + + private CompositeHook(@Nonnull Collection hooks) { + this.hooks = hooks; + } + + public CompositeHook(CommitHook... hooks) { + this(Arrays.asList(hooks)); + } + + @Override + public NodeState processCommit(NodeState before, NodeState after) + throws CommitFailedException { + NodeState newState = after; + for (CommitHook hook : hooks) { + newState = hook.processCommit(before, newState); + } + return newState; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/CompositeValidator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/CompositeValidator.java new file mode 100644 index 00000000000..7c7a0709d09 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/CompositeValidator.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.commit; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * This {@code Validator} aggregates a list of validators into + * a single validator. + */ +public class CompositeValidator implements Validator { + private final List validators; + + public CompositeValidator(List validators) { + this.validators = validators; + } + + public CompositeValidator(Validator... validators) { + this(Arrays.asList(validators)); + } + + @Override + public void propertyAdded(PropertyState after) throws CommitFailedException { + for (Validator validator : validators) { + validator.propertyAdded(after); + } + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) + throws CommitFailedException { + for (Validator validator : validators) { + validator.propertyChanged(before, after); + } + } + + @Override + public void propertyDeleted(PropertyState before) throws CommitFailedException { + for (Validator validator : validators) { + validator.propertyDeleted(before); + } + } + + @Override + public Validator childNodeAdded(String name, NodeState after) throws CommitFailedException { + List childValidators = new ArrayList(validators.size()); + for (Validator validator : validators) { + Validator child = validator.childNodeAdded(name, after); + if (child != null) { + childValidators.add(child); + } + } + if (!childValidators.isEmpty()) { + return new CompositeValidator(childValidators); + } else { + return null; + } + } + + @Override + public Validator childNodeChanged(String name, NodeState before, NodeState after) throws CommitFailedException { + List childValidators = new ArrayList(validators.size()); + for (Validator validator : validators) { + Validator child = validator.childNodeChanged(name, before, after); + if (child != null) { + childValidators.add(child); + } + } + if (!childValidators.isEmpty()) { + return new CompositeValidator(childValidators); + } else { + return null; + } + } + + @Override + public Validator childNodeDeleted(String name, NodeState before) throws CommitFailedException { + List childValidators = new ArrayList(validators.size()); + for (Validator validator : validators) { + Validator child = validator.childNodeDeleted(name, before); + if (child != null) { + childValidators.add(child); + } + } + if (!childValidators.isEmpty()) { + return new CompositeValidator(childValidators); + } else { + return null; + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/CompositeValidatorProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/CompositeValidatorProvider.java new file mode 100644 index 00000000000..f9a5ba26ba5 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/CompositeValidatorProvider.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.spi.commit; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * This {@code ValidatorProvider} aggregates a list of validator providers into + * a single validator provider. + */ +public class CompositeValidatorProvider implements ValidatorProvider { + + public static ValidatorProvider compose(@Nonnull Collection providers) { + switch (providers.size()) { + case 0: + return DefaultValidatorProvider.INSTANCE; + case 1: + return providers.iterator().next(); + default: + return new CompositeValidatorProvider(providers); + } + } + + private final Collection providers; + + private CompositeValidatorProvider(Collection providers) { + this.providers = providers; + } + + public CompositeValidatorProvider(ValidatorProvider... providers) { + this(Arrays.asList(providers)); + } + + @Override + public Validator getRootValidator(NodeState before, NodeState after) { + List rootValidators = new ArrayList(providers.size()); + + for (ValidatorProvider provider : providers) { + rootValidators.add(provider.getRootValidator(before, after)); + } + + return new CompositeValidator(rootValidators); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/ConflictHandler.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/ConflictHandler.java new file mode 100644 index 00000000000..dde421db74f --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/ConflictHandler.java @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.spi.commit; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * A {@code ConflictHandler} is responsible for handling conflicts which happen + * on {@link org.apache.jackrabbit.oak.api.Root#rebase()} and on the implicit rebase operation which + * takes part on {@link org.apache.jackrabbit.oak.api.Root#commit()}. + * + * This interface contains one method per type of conflict which might occur. + * Each of these methods must return a {@link Resolution} for the current conflict. + * The resolution indicates to use the changes in the current {@code Root} instance + * ({@link Resolution#OURS}) or to use the changes from the underlying persistence + * store ({@link Resolution#THEIRS}). Alternatively the resolution can also indicate + * that the changes have been successfully merged by this {@code ConflictHandler} + * instance ({@link Resolution#MERGED}). + */ +public interface ConflictHandler { + + /** + * Resolutions for conflicts + */ + enum Resolution { + /** + * Use the changes from the current {@link org.apache.jackrabbit.oak.api.Root} instance + */ + OURS, + + /** + * Use the changes from the underlying persistence store + */ + THEIRS, + + /** + * Indicated changes have been merged by this {@code ConflictHandler} instance. + */ + MERGED + } + + /** + * The property {@code ours} has been added to {@code parent} which conflicts + * with property {@code theirs} which has been added in the persistence store. + * + * @param parent root of the conflict + * @param ours our version of the property + * @param theirs their version of the property + * @return {@link Resolution} of the conflict + */ + Resolution addExistingProperty(NodeBuilder parent, PropertyState ours, PropertyState theirs); + + /** + * The property {@code ours} has been changed in {@code parent} while it was + * removed in the persistence store. + * + * @param parent root of the conflict + * @param ours our version of the property + * @return {@link Resolution} of the conflict + */ + Resolution changeDeletedProperty(NodeBuilder parent, PropertyState ours); + + /** + * The property {@code ours} has been changed in {@code parent} while it was + * also changed to a different value ({@code theirs}) in the persistence store. + * + * @param parent root of the conflict + * @param ours our version of the property + * @param theirs their version of the property + * @return {@link Resolution} of the conflict + */ + Resolution changeChangedProperty(NodeBuilder parent, PropertyState ours, PropertyState theirs); + + /** + * The property {@code ours} has been removed in {@code parent} while it was + * also removed in the persistence store. + * + * @param parent root of the conflict + * @param ours our version of the property + * @return {@link Resolution} of the conflict + */ + Resolution deleteDeletedProperty(NodeBuilder parent, PropertyState ours); + + /** + * The property {@code theirs} changed in the persistence store while it has been + * deleted locally. + * + * @param parent root of the conflict + * @param theirs their version of the property + * @return {@link Resolution} of the conflict + */ + Resolution deleteChangedProperty(NodeBuilder parent, PropertyState theirs); + + /** + * The node {@code ours} has been added to {@code parent} which conflicts + * with node {@code theirs} which has been added in the persistence store. + * + * @param parent root of the conflict + * @param name name of the node + * @param ours our version of the node + * @param theirs their version of the node + * @return {@link Resolution} of the conflict + */ + Resolution addExistingNode(NodeBuilder parent, String name, NodeState ours, NodeState theirs); + + /** + * The node {@code ours} has been changed in {@code parent} while it was + * removed in the persistence store. + * + * @param parent root of the conflict + * @param name name of the node + * @param ours our version of the node + * @return {@link Resolution} of the conflict + */ + Resolution changeDeletedNode(NodeBuilder parent, String name, NodeState ours); + + /** + * The node {@code theirs} changed in the persistence store while it has been + * deleted locally. + * + * @param parent root of the conflict + * @param name name of the node + * @param theirs their version of the node + * @return {@link Resolution} of the conflict + */ + Resolution deleteChangedNode(NodeBuilder parent, String name, NodeState theirs); + + /** + * The node {@code name} has been removed in {@code parent} while it was + * also removed in the persistence store. + * + * @param parent root of the conflict + * @param name name of the node + * @return {@link Resolution} of the conflict + */ + Resolution deleteDeletedNode(NodeBuilder parent, String name); +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/ConflictHandlerProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/ConflictHandlerProvider.java new file mode 100644 index 00000000000..d00b11a3fbe --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/ConflictHandlerProvider.java @@ -0,0 +1,5 @@ +package org.apache.jackrabbit.oak.spi.commit; + +public interface ConflictHandlerProvider { + ConflictHandler getConflictHandler(); +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/DefaultValidator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/DefaultValidator.java new file mode 100644 index 00000000000..afc61ed3ef4 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/DefaultValidator.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.spi.commit; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * Validator that does nothing by default and doesn't recurse into subtrees. + * Useful as a sentinel or as a base class for more complex validators. + * + * @since Oak 0.3 + */ +public class DefaultValidator implements Validator { + + public static final Validator INSTANCE = new DefaultValidator(); + + @Override + public void propertyAdded(PropertyState after) + throws CommitFailedException { + // do nothing + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) + throws CommitFailedException { + // do nothing + } + + @Override + public void propertyDeleted(PropertyState before) + throws CommitFailedException { + // do nothing + } + + @Override + public Validator childNodeAdded(String name, NodeState after) + throws CommitFailedException { + return null; + } + + @Override + public Validator childNodeChanged( + String name, NodeState before, NodeState after) + throws CommitFailedException { + return null; + } + + @Override + public Validator childNodeDeleted(String name, NodeState before) + throws CommitFailedException { + return null; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/DefaultValidatorProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/DefaultValidatorProvider.java new file mode 100644 index 00000000000..024bbdb5614 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/DefaultValidatorProvider.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.commit; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * Validator provider that returns a new instance of {@link DefaultValidator}. + */ +public class DefaultValidatorProvider implements ValidatorProvider { + + public static final ValidatorProvider INSTANCE = + new DefaultValidatorProvider(); + + @Override @Nonnull + public Validator getRootValidator(NodeState before, NodeState after) { + return DefaultValidator.INSTANCE; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/EmptyHook.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/EmptyHook.java new file mode 100644 index 00000000000..6bf59d2f743 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/EmptyHook.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.commit; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * Basic commit hook implementation that by default doesn't do anything. + * This class has a dual purpose: + *

      + *
    1. The static {@link #INSTANCE} instance can be used as a "null object" + * in cases where another commit hook has not been configured, thus avoiding + * the need for extra code for such cases.
    2. + *
    3. Other commit hook implementations can extend this class and gain + * improved forwards-compatibility to possible changes in the + * {@link CommitHook} interface. For example if it is later decided that + * new arguments are needed in the hook methods, this class is guaranteed + * to implement any new method signatures in a way that falls gracefully + * back to any earlier behavior.
    4. + *
    + */ +public class EmptyHook implements CommitHook { + + /** + * Static instance of this class, useful as a "null object". + */ + public static final CommitHook INSTANCE = new EmptyHook(); + + @Override + public NodeState processCommit(NodeState before, NodeState after) + throws CommitFailedException { + return after; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/EmptyObserver.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/EmptyObserver.java new file mode 100644 index 00000000000..293c1cb1f02 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/EmptyObserver.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.commit; + +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * Basic content change observer that doesn't do anything. Useful as a + * "null object" for cases where another observer has not been configured, + * thus avoiding an extra {@code null} check when invoking the observer. + */ +public class EmptyObserver implements Observer { + + /** + * Static instance of this class, useful as a "null object". + */ + public static final EmptyObserver INSTANCE = new EmptyObserver(); + + @Override + public void contentChanged(NodeState before, NodeState after) { + // do nothing + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/FailingValidator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/FailingValidator.java new file mode 100644 index 00000000000..563889428b5 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/FailingValidator.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.commit; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * Validator that rejects all changes. Useful as a sentinel or as + * a tool for testing composite validators. + * + * @since Oak 0.3 + */ +public class FailingValidator implements Validator { + + private final String message; + + public FailingValidator() { + this("All changes are rejected"); + } + + public FailingValidator(String message) { + this.message = message; + } + + @Override + public void propertyAdded(PropertyState after) + throws CommitFailedException { + throw new CommitFailedException(message); + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) + throws CommitFailedException { + throw new CommitFailedException(message); + } + + @Override + public void propertyDeleted(PropertyState before) + throws CommitFailedException { + throw new CommitFailedException(message); + } + + @Override + public Validator childNodeAdded(String name, NodeState after) + throws CommitFailedException { + throw new CommitFailedException(message); + } + + @Override + public Validator childNodeChanged( + String name, NodeState before, NodeState after) + throws CommitFailedException { + throw new CommitFailedException(message); + } + + @Override + public Validator childNodeDeleted(String name, NodeState before) + throws CommitFailedException { + throw new CommitFailedException(message); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/Observer.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/Observer.java new file mode 100644 index 00000000000..a7466287eb8 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/Observer.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.commit; + +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * Extension point for observing changes in an Oak repository. Content + * changes are reported by passing the "before" and "after" state of the + * content tree to the {@link #contentChanged(NodeState, NodeState)} + * callback method. + *

    + * Each observer is guaranteed to see a linear sequence of changes, i.e. + * the "after" state of one method call is guaranteed to be the "before" + * state of the following call. This sequence of changes only applies while + * a hook is registered with a specific repository instance, and is thus for + * example not guaranteed across repository restarts. + *

    + * Note also that two observers may not necessarily see the same sequence of + * content changes, and each commit does not necessarily trigger a separate + * observer callback. It is also possible for an observer to be notified + * when no actual changes have been committed. + *

    + * A specific implementation or deployment may offer more guarantees about + * when and how observers are notified of content changes. See the relevant + * documentation for more details about such cases. + * + * @since Oak 0.3 + */ +public interface Observer { + + /** + * Observes a content change. The implementation might for example + * use this information to update caches, trigger JCR-level observation + * events or otherwise record the change. + *

    + * Content changes are observed for both commits made locally against + * the repository instance to which the hook is registered and any + * other changes committed by other repository instances in the same + * cluster. + *

    + * After-commit hooks are executed synchronously within the context of + * a repository instance, so to prevent delaying access to latest changes + * the after-commit hooks should avoid any potentially blocking + * operations. + * + * @param before content tree before the changes + * @param after content tree after the changes + */ + void contentChanged(NodeState before, NodeState after); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/SubtreeValidator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/SubtreeValidator.java new file mode 100644 index 00000000000..47e59719436 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/SubtreeValidator.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.spi.commit; + +import java.util.Arrays; +import java.util.List; + +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Validator that detects changes to a specified subtree and delegates the + * validation of such changes to another given validator. + * + * @since Oak 0.3 + */ +public class SubtreeValidator extends DefaultValidator { + + private final Validator validator; + + private final String head; + + private final List tail; + + public SubtreeValidator(Validator validator, String... path) { + this(validator, Arrays.asList(path)); + } + + private SubtreeValidator(Validator validator, List path) { + this.validator = checkNotNull(validator); + checkNotNull(path); + checkArgument(!path.isEmpty()); + this.head = path.get(0); + this.tail = path.subList(1, path.size()); + } + + @Override + public Validator childNodeAdded(String name, NodeState after) { + return descend(name); + } + + @Override + public Validator childNodeChanged( + String name, NodeState before, NodeState after) { + return descend(name); + } + + @Override + public Validator childNodeDeleted(String name, NodeState before) { + return descend(name); + } + + private Validator descend(String name) { + if (!head.equals(name)) { + return null; + } else if (tail.isEmpty()) { + return validator; + } else { + return new SubtreeValidator(validator, tail); + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/ValidatingHook.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/ValidatingHook.java new file mode 100644 index 00000000000..7a13ff87672 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/ValidatingHook.java @@ -0,0 +1,197 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.commit; + +import java.util.Arrays; +import java.util.List; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; + +import static org.apache.jackrabbit.oak.plugins.memory.MemoryNodeState.EMPTY_NODE; + +/** + * This commit hook implementation validates the changes to be committed + * against the {@link Validator} provided by the {@link ValidatorProvider} + * passed to the class' constructor. + */ +public class ValidatingHook implements CommitHook { + + private final ValidatorProvider validatorProvider; + + /** + * Create a new commit hook which validates the commit against all + * {@link Validator}s provided by {@code validatorProvider}. + * @param validatorProvider validator provider + */ + public ValidatingHook(ValidatorProvider validatorProvider) { + this.validatorProvider = validatorProvider; + } + + public ValidatingHook(ValidatorProvider... providers) { + this(new CompositeValidatorProvider(providers)); + } + + public ValidatingHook(final Validator validator) { + this(new ValidatorProvider() { + @Override + public Validator getRootValidator( + NodeState before, NodeState after) { + return validator; + } + }); + } + + public ValidatingHook(List validators) { + this(new CompositeValidator(validators)); + } + + public ValidatingHook(Validator... validators) { + this(Arrays.asList(validators)); + } + + @Override + public NodeState processCommit(NodeState before, NodeState after) + throws CommitFailedException { + Validator validator = validatorProvider.getRootValidator(before, after); + ValidatorDiff.validate(validator, before, after); + return after; + } + + //------------------------------------------------------------< private >--- + + private static class ValidatorDiff implements NodeStateDiff { + + private final Validator validator; + + /** + * Checked exceptions don't compose. So we need to hack around. + * See http://markmail.org/message/ak67n5k7mr3vqylm and + * http://markmail.org/message/bhocbruikljpuhu6 + */ + private CommitFailedException exception; + + /** + * Validates the given subtree by diffing and recursing through it. + * + * @param validator validator for the root of the subtree + * @param before state of the original subtree + * @param after state of the modified subtree + * @throws CommitFailedException if validation failed + */ + public static void validate( + Validator validator, NodeState before, NodeState after) + throws CommitFailedException { + new ValidatorDiff(validator).validate(before, after); + } + + private ValidatorDiff(Validator validator) { + this.validator = validator; + } + + private void validate(NodeState before, NodeState after) + throws CommitFailedException { + after.compareAgainstBaseState(before, this); + if (exception != null) { + throw exception; + } + } + + //-------------------------------------------------< NodeStateDiff >-- + + @Override + public void propertyAdded(PropertyState after) { + if (exception == null) { + try { + validator.propertyAdded(after); + } catch (CommitFailedException e) { + exception = e; + } + } + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) { + if (exception == null) { + try { + validator.propertyChanged(before, after); + } catch (CommitFailedException e) { + exception = e; + } + } + } + + @Override + public void propertyDeleted(PropertyState before) { + if (exception == null) { + try { + validator.propertyDeleted(before); + } catch (CommitFailedException e) { + exception = e; + } + } + } + + @Override + public void childNodeAdded(String name, NodeState after) { + if (exception == null) { + try { + Validator v = validator.childNodeAdded(name, after); + if (v != null) { + validate(v, EMPTY_NODE, after); + } + } catch (CommitFailedException e) { + exception = e; + } + } + } + + @Override + public void childNodeChanged( + String name, NodeState before, NodeState after) { + if (exception == null) { + try { + Validator v = + validator.childNodeChanged(name, before, after); + if (v != null) { + validate(v, before, after); + } + } catch (CommitFailedException e) { + exception = e; + } + } + } + + @Override + public void childNodeDeleted(String name, NodeState before) { + if (exception == null) { + try { + Validator v = validator.childNodeDeleted(name, before); + if (v != null) { + validate(v, before, EMPTY_NODE); + } + } catch (CommitFailedException e) { + exception = e; + } + } + } + + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/Validator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/Validator.java new file mode 100644 index 00000000000..8d1afacfa5d --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/Validator.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.commit; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import javax.annotation.CheckForNull; + +/** + * Content change validator. An instance of this interface is used to + * validate changes against a specific {@link NodeState}. + */ +public interface Validator { + + /** + * Validate an added property + * @param after the added property + * @throws CommitFailedException if validation fails. + */ + void propertyAdded(PropertyState after) + throws CommitFailedException; + + /** + * Validate a changed property + * @param before the original property + * @param after the changed property + * @throws CommitFailedException if validation fails. + */ + void propertyChanged(PropertyState before, PropertyState after) + throws CommitFailedException; + + /** + * Validate a deleted property + * @param before the original property + * @throws CommitFailedException if validation fails. + */ + void propertyDeleted(PropertyState before) + throws CommitFailedException; + + /** + * Validate an added node + * @param name the name of the added node + * @param after the added node + * @return a {@code Validator} for {@code after} or {@code null} if validation + * should not decent into the subtree rooted at {@code after}. + * @throws CommitFailedException if validation fails. + */ + @CheckForNull + Validator childNodeAdded(String name, NodeState after) + throws CommitFailedException; + + /** + * Validate a changed node + * @param name the name of the changed node + * @param before the original node + * @param after the changed node + * @return a {@code Validator} for {@code after} or {@code null} if validation + * should not decent into the subtree rooted at {@code after}. + * @throws CommitFailedException if validation fails. + */ + @CheckForNull + Validator childNodeChanged( + String name, NodeState before, NodeState after) + throws CommitFailedException; + + /** + * Validate a deleted node + * @param name The name of the deleted node. + * @param before the original node + * @return a {@code Validator} for the removed subtree or + * {@code null} if validation should not decent into the subtree + * @throws CommitFailedException if validation fails. + */ + @CheckForNull + Validator childNodeDeleted(String name, NodeState before) + throws CommitFailedException; + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/ValidatorProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/ValidatorProvider.java new file mode 100644 index 00000000000..8162867c424 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/ValidatorProvider.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.spi.commit; + +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import javax.annotation.Nonnull; + +/** + * Extension point for plugging in different kinds of validation rules + * for content changes. + */ +public interface ValidatorProvider { + + /** + * Returns a validator for checking the changes between the given + * two root states. + * + * @param before original root state + * @param after modified root state + * @return validator for checking the modifications + */ + @Nonnull + Validator getRootValidator(NodeState before, NodeState after); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/lifecycle/CompositeInitializer.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/lifecycle/CompositeInitializer.java new file mode 100644 index 00000000000..b9fcabd02ae --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/lifecycle/CompositeInitializer.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.spi.lifecycle; + +import java.util.Arrays; +import java.util.Collection; + +import org.apache.jackrabbit.oak.spi.state.NodeStore; + +/** + * Composite repository initializer that delegates the + * {@link #initialize(NodeStore)} call in sequence to all the + * component initializers. + */ +public class CompositeInitializer implements RepositoryInitializer { + + private final Collection initializers; + + public CompositeInitializer(Collection trackers) { + this.initializers = trackers; + } + + public CompositeInitializer(RepositoryInitializer... initializers) { + this.initializers = Arrays.asList(initializers); + } + + @Override + public void initialize(NodeStore store) { + for (RepositoryInitializer tracker : initializers) { + tracker.initialize(store); + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/lifecycle/RepositoryInitializer.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/lifecycle/RepositoryInitializer.java new file mode 100644 index 00000000000..f2b0e69fb0d --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/lifecycle/RepositoryInitializer.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.spi.lifecycle; + +import org.apache.jackrabbit.oak.spi.state.NodeStore; + +/** + * Initializer of repository content. A component that needs to add specific + * content to a new repository can implement this interface. Then when a + * repository becomes available, all the configured initializers are invoked + * in sequence. + */ +public interface RepositoryInitializer { + + /** + * Initializes repository content. This method is called as soon as a + * repository becomes available. Note that the repository may already + * have been initialized, so the implementation of this method should + * check for that before blindly adding new content. + * + * @param store node store of the repository + */ + public void initialize(NodeStore store); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/observation/ChangeExtractor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/observation/ChangeExtractor.java new file mode 100644 index 00000000000..3ed65affd5d --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/observation/ChangeExtractor.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.spi.observation; + +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; + +/** + * An instance of {@code ChangeExtractor} can be used to follow changes + * done to a {@link org.apache.jackrabbit.oak.api.Root} instance. + */ +public interface ChangeExtractor { + + /** + * Get the most recent changes. + * @param diff {@code NodeStateDiff} to receive the changes + */ + void getChanges(NodeStateDiff diff); +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/CompositeQueryIndexProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/CompositeQueryIndexProvider.java new file mode 100644 index 00000000000..5d749ac9b00 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/CompositeQueryIndexProvider.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.query; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +/** + * This {@code QueryIndexProvider} aggregates a list of query index providers + * into a single query index provider. + */ +public class CompositeQueryIndexProvider implements QueryIndexProvider { + + @Nonnull + public static QueryIndexProvider compose( + @Nonnull Collection providers) { + if (providers.isEmpty()) { + return new QueryIndexProvider() { + @Override + public List getQueryIndexes(NodeState nodeState) { + return ImmutableList.of(); + } + }; + } else if (providers.size() == 1) { + return providers.iterator().next(); + } else { + return new CompositeQueryIndexProvider( + ImmutableList.copyOf(providers)); + } + } + + private final List providers; + + private CompositeQueryIndexProvider(List providers) { + this.providers = providers; + } + + public CompositeQueryIndexProvider(QueryIndexProvider... providers) { + this(Arrays.asList(providers)); + } + + @Override @Nonnull + public List getQueryIndexes(NodeState nodeState) { + List indexes = Lists.newArrayList(); + for (QueryIndexProvider provider : providers) { + indexes.addAll(provider.getQueryIndexes(nodeState)); + } + return indexes; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/Cursor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/Cursor.java new file mode 100644 index 00000000000..74527c1ea14 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/Cursor.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.spi.query; + +/** + * A cursor to read a number of nodes sequentially. + */ +public interface Cursor { + + /** + * Skip to the next node if one is available. + * + * @return true if another row is available + */ + boolean next(); + + /** + * The current row within this index. + *

    + * The row may only contains the path, if a path is available. It may also + * (or just) contain so-called "pseudo-properties" such as "jcr:score" and + * "rep:excerpt", in case the index supports those properties and if the + * properties were requested when running the query. The query engine will + * indicate that those pseudo properties were requested by setting an + * appropriate (possibly unrestricted) filter condition. + *

    + * The index should return a row with those properties that are stored in + * the index itself, so that the query engine doesn't have to load the whole + * row / node unnecessarily (avoiding to load the whole row is sometimes + * called "index only scan"), specially for rows that are anyway skipped. If + * the index does not have an (efficient) way to return some (or any) of the + * properties, it doesn't have to provide those values. In this case, the + * query engine will load the node itself if required. If all conditions + * match, the query engine will sometimes load the node to do access checks, + * but this is not always the case, and it is not the case if any of the + * (join) conditions do not match. + * + * @return the row + */ + IndexRow currentRow(); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/Filter.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/Filter.java new file mode 100644 index 00000000000..189554b0011 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/Filter.java @@ -0,0 +1,158 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.spi.query; + +import java.util.Collection; + +import javax.jcr.PropertyType; + +import org.apache.jackrabbit.oak.api.PropertyValue; + +/** + * The filter for an index lookup. + */ +public interface Filter { + + /** + * Get the list of property restrictions, if any. + * + * @return the conditions (an empty collection if not used) + */ + Collection getPropertyRestrictions(); + + /** + * Get the fulltext search conditions, if any. + * + * @return the conditions (an empty collection if not used) + */ + Collection getFulltextConditions(); + + /** + * Get the property restriction for the given property, if any. + * + * @param propertyName the property name + * @return the restriction, or null if there is no restriction for this property + */ + PropertyRestriction getPropertyRestriction(String propertyName); + + /** + * Get the path restriction type. + * + * @return the path restriction type + */ + PathRestriction getPathRestriction(); + + /** + * Get the path, or "/" if there is no path restriction set. + * + * @return the path + */ + String getPath(); + + String getNodeType(); + + /** + * A restriction for a property. + */ + class PropertyRestriction { + + /** + * The name of the property. + */ + public String propertyName; + + /** + * The first value to read, or null to read from the beginning. + */ + public PropertyValue first; + + /** + * Whether values that match the first should be returned. + */ + public boolean firstIncluding; + + /** + * The last value to read, or null to read until the end. + */ + public PropertyValue last; + + /** + * Whether values that match the last should be returned. + */ + public boolean lastIncluding; + + /** + * Whether this is a like constraint. in this case only the 'first' + * value should be taken into consideration + */ + public boolean isLike; + + /** + * The property type, if restricted. + * If not restricted, this field is set to PropertyType.UNDEFINED. + */ + public int propertyType = PropertyType.UNDEFINED; + + @Override + public String toString() { + return (first == null ? "" : ((firstIncluding ? "[" : "(") + first)) + ".." + + (last == null ? "" : last + (lastIncluding ? "]" : ")")); + } + + } + + /** + * The path restriction type. + */ + enum PathRestriction { + + /** + * A parent of this node + */ + PARENT("/.."), + + /** + * This exact node only. + */ + EXACT(""), + + /** + * All direct child nodes. + */ + DIRECT_CHILDREN("/*"), + + /** + * All direct and indirect child nodes. + */ + ALL_CHILDREN("//*"); + + private final String name; + + PathRestriction(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/IndexRow.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/IndexRow.java new file mode 100644 index 00000000000..95fe2cdb222 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/IndexRow.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.spi.query; + + +import org.apache.jackrabbit.oak.spi.query.PropertyStateValue; + +/** + * A row returned by the index. + */ +public interface IndexRow { + + /** + * The path of the node, if available. + * + * @return the path + */ + String getPath(); + + /** + * The value of the given property, if available. This might be a property + * of the given node, or a pseudo-property (a property that is only + * available in the index but not in the node itself, such as "jcr:score"). + * + * @param columnName the column name + * @return the value, or null if not available + */ + PropertyStateValue getValue(String columnName); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/PropertyStateValue.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/PropertyStateValue.java new file mode 100644 index 00000000000..861206a1bd0 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/PropertyStateValue.java @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.spi.query; + +import java.util.Calendar; +import java.util.Iterator; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.jcr.PropertyType; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.util.ISO8601; + +/** + * A {@link PropertyValue} implementation that wraps a {@link PropertyState} + * + */ +public class PropertyStateValue implements PropertyValue { + + private final PropertyState ps; + + protected PropertyStateValue(PropertyState ps) { + this.ps = ps; + } + + public boolean isArray() { + return ps.isArray(); + } + + @Nonnull + public Type getType() { + return ps.getType(); + } + + @Nonnull + public T getValue(Type type) { + return ps.getValue(type); + } + + @Nonnull + public T getValue(Type type, int index) { + return ps.getValue(type, index); + } + + public long size() { + return ps.size(); + } + + public long size(int index) { + return ps.size(index); + } + + public int count() { + return ps.count(); + } + + @CheckForNull + public PropertyState unwrap() { + return ps; + } + + @Override + public int compareTo(PropertyValue p2) { + if (getType().tag() != p2.getType().tag()) { + return Integer.signum(getType().tag() - p2.getType().tag()); + } + switch (getType().tag()) { + case PropertyType.BINARY: + return compare(getValue(Type.BINARIES), p2.getValue(Type.BINARIES)); + case PropertyType.DOUBLE: + return compare(getValue(Type.DOUBLES), p2.getValue(Type.DOUBLES)); + case PropertyType.DATE: + return compareAsDate(getValue(Type.STRINGS), + p2.getValue(Type.STRINGS)); + default: + return compare(getValue(Type.STRINGS), p2.getValue(Type.STRINGS)); + } + } + + private static > int compare(Iterable p1, + Iterable p2) { + Iterator i1 = p1.iterator(); + Iterator i2 = p2.iterator(); + while (i1.hasNext() || i2.hasNext()) { + if (!i1.hasNext()) { + return 1; + } + if (!i2.hasNext()) { + return -1; + } + int compare = i1.next().compareTo(i2.next()); + if (compare != 0) { + return compare; + } + } + return 0; + } + + private static int compareAsDate(Iterable p1, Iterable p2) { + Iterator i1 = p1.iterator(); + Iterator i2 = p2.iterator(); + while (i1.hasNext() || i2.hasNext()) { + if (!i1.hasNext()) { + return 1; + } + if (!i2.hasNext()) { + return -1; + } + String v1 = i1.next(); + String v2 = i2.next(); + + Calendar c1 = ISO8601.parse(v1); + Calendar c2 = ISO8601.parse(v2); + int compare = -1; + if (c1 != null && c2 != null) { + compare = c1.compareTo(c2); + } else { + compare = v1.compareTo(v2); + } + if (compare != 0) { + return compare; + } + } + return 0; + } + + // --------------------------------------------------------------< Object > + + private String getInternalString() { + StringBuilder sb = new StringBuilder(); + Iterator iterator = getValue(Type.STRINGS).iterator(); + while (iterator.hasNext()) { + sb.append(iterator.next()); + if (iterator.hasNext()) { + sb.append(","); + } + } + return sb.toString(); + } + + @Override + public int hashCode() { + return getType().tag() ^ getInternalString().hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } else if (o instanceof PropertyStateValue) { + return compareTo((PropertyStateValue) o) == 0; + } else { + return false; + } + } + + @Override + public String toString() { + return getInternalString(); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/PropertyValues.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/PropertyValues.java new file mode 100644 index 00000000000..da8f6a4ec62 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/PropertyValues.java @@ -0,0 +1,265 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.spi.query; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.Iterator; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.jcr.PropertyType; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.plugins.memory.BinaryPropertyState; +import org.apache.jackrabbit.oak.plugins.memory.BooleanPropertyState; +import org.apache.jackrabbit.oak.plugins.memory.DecimalPropertyState; +import org.apache.jackrabbit.oak.plugins.memory.DoublePropertyState; +import org.apache.jackrabbit.oak.plugins.memory.GenericPropertyState; +import org.apache.jackrabbit.oak.plugins.memory.LongPropertyState; +import org.apache.jackrabbit.oak.plugins.memory.MultiStringPropertyState; +import org.apache.jackrabbit.oak.plugins.memory.StringPropertyState; + +/** + * Utility class for creating {@link PropertyValue} instances. + */ +public final class PropertyValues { + + private PropertyValues() { + } + + @CheckForNull + public static PropertyValue create(PropertyState property) { + if (property == null) { + return null; + } + return new PropertyStateValue(property); + } + + @CheckForNull + public static PropertyState create(PropertyValue value) { + if (value == null) { + return null; + } + if (value instanceof PropertyStateValue) { + return ((PropertyStateValue) value).unwrap(); + } + return null; + } + + @Nonnull + public static PropertyValue newString(String value) { + return new PropertyStateValue(StringPropertyState.stringProperty("", value)); + } + + @Nonnull + public static PropertyValue newString(Iterable value) { + return new PropertyStateValue(MultiStringPropertyState.stringProperty("", value)); + } + + @Nonnull + public static PropertyValue newLong(Long value) { + return new PropertyStateValue(LongPropertyState.createLongProperty("", value)); + } + + @Nonnull + public static PropertyValue newDouble(Double value) { + return new PropertyStateValue(DoublePropertyState.doubleProperty("", value)); + } + + @Nonnull + public static PropertyValue newDecimal(BigDecimal value) { + return new PropertyStateValue(DecimalPropertyState.decimalProperty("", value)); + } + + @Nonnull + public static PropertyValue newBoolean(boolean value) { + return new PropertyStateValue(BooleanPropertyState.booleanProperty("", value)); + } + + @Nonnull + public static PropertyValue newDate(String value) { + return new PropertyStateValue(LongPropertyState.createDateProperty("", value)); + } + + @Nonnull + public static PropertyValue newName(String value) { + return new PropertyStateValue(GenericPropertyState.nameProperty("", value)); + } + + @Nonnull + public static PropertyValue newPath(String value) { + return new PropertyStateValue(GenericPropertyState.pathProperty("", value)); + } + + @Nonnull + public static PropertyValue newReference(String value) { + return new PropertyStateValue(GenericPropertyState.referenceProperty("", value)); + } + + @Nonnull + public static PropertyValue newWeakReference(String value) { + return new PropertyStateValue(GenericPropertyState.weakreferenceProperty("", value)); + } + + @Nonnull + public static PropertyValue newUri(String value) { + return new PropertyStateValue(GenericPropertyState.uriProperty("", value)); + } + + @Nonnull + public static PropertyValue newBinary(byte[] value) { + return new PropertyStateValue(BinaryPropertyState.binaryProperty("", value)); + } + + // -- + + public static boolean match(PropertyValue p1, PropertyState p2) { + return match(p1, create(p2)); + } + + public static boolean match(PropertyState p1, PropertyValue p2) { + return match(create(p1), p2); + } + + public static boolean match(PropertyValue p1, PropertyValue p2) { + if (p1.getType().tag() != p2.getType().tag()) { + return false; + } + + switch (p1.getType().tag()) { + case PropertyType.BINARY: + if (p1.isArray() && !p2.isArray()) { + return contains(p1.getValue(Type.BINARIES), + p2.getValue(Type.BINARY)); + } + if (!p1.isArray() && p2.isArray()) { + return contains(p2.getValue(Type.BINARIES), + p2.getValue(Type.BINARY)); + } + default: + if (p1.isArray() && !p2.isArray()) { + return contains(p1.getValue(Type.STRINGS), + p2.getValue(Type.STRING)); + } + if (!p1.isArray() && p2.isArray()) { + return contains(p2.getValue(Type.STRINGS), + p2.getValue(Type.STRING)); + } + } + // both arrays or both single values + return p1.compareTo(p2) == 0; + + } + + private static > boolean contains(Iterable p1, + T p2) { + Iterator i1 = p1.iterator(); + while (i1.hasNext()) { + int compare = i1.next().compareTo(p2); + if (compare == 0) { + return true; + } + } + return false; + } + + // -- + /** + * Convert a value to the given target type, if possible. + * + * @param value + * the value to convert + * @param targetType + * the target property type + * @return the converted value, or null if converting is not possible + */ + public static PropertyValue convert(PropertyValue value, int targetType, + NamePathMapper mapper) { + // TODO support full set of conversion features defined in the JCR spec + // at 3.6.4 Property Type Conversion + // re-use existing code if possible + try { + switch (targetType) { + case PropertyType.STRING: + return newString(value.getValue(Type.STRING)); + case PropertyType.DATE: + return newDate(value.getValue(Type.STRING)); + case PropertyType.LONG: + return newLong(value.getValue(Type.LONG)); + case PropertyType.DOUBLE: + return newDouble(value.getValue(Type.DOUBLE)); + case PropertyType.DECIMAL: + return newDecimal(value.getValue(Type.DECIMAL)); + case PropertyType.BOOLEAN: + return newBoolean(value.getValue(Type.BOOLEAN)); + case PropertyType.NAME: + return newName(getOakPath(value.getValue(Type.STRING), mapper)); + case PropertyType.PATH: + return newPath(value.getValue(Type.STRING)); + case PropertyType.REFERENCE: + return newReference(value.getValue(Type.STRING)); + case PropertyType.WEAKREFERENCE: + return newWeakReference(value.getValue(Type.STRING)); + case PropertyType.URI: + return newUri(value.getValue(Type.STRING)); + case PropertyType.BINARY: + try { + byte[] data = value.getValue(Type.STRING).getBytes("UTF-8"); + return newBinary(data); + } catch (IOException e) { + // I don't know in what case that could really occur + // except if UTF-8 isn't supported + throw new IllegalArgumentException( + value.getValue(Type.STRING), e); + } + } + return null; + // throw new IllegalArgumentException("Unknown property type: " + + // targetType); + } catch (UnsupportedOperationException e) { + // TODO detect unsupported conversions, so that no exception is + // thrown + // because exceptions are slow + return null; + // throw new IllegalArgumentException(""); + } + } + + public static String getOakPath(String jcrPath, NamePathMapper mapper) { + if (mapper == null) { + // to simplify testing, a getNamePathMapper isn't required + return jcrPath; + } + String p = mapper.getOakPath(jcrPath); + if (p == null) { + throw new IllegalArgumentException("Not a valid JCR path: " + + jcrPath); + } + return p; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryIndex.java new file mode 100644 index 00000000000..273d980e51b --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryIndex.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.spi.query; + +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * Represents an index. The index should use the data in the filter if possible + * to speed up reading. + *

    + * The query engine will pick the index that returns the lowest cost for the + * given filter conditions. + *

    + * The index should only use that part of the filter that speeds up data lookup. + * All other filter conditions should be ignored and not evaluated within this + * index, because the query engine will in any case evaluate the condition (and + * join condition), so that evaluating the conditions within the index would + * actually slow down processing. For example, an index on the property + * "lastName" should not try to evaluate any other restrictions than those on + * the property "lastName", even if the query contains other restrictions. For + * the query "where lastName = 'x' and firstName = 'y'", the query engine will + * set two filter conditions, one for "lastName" and another for "firstName". + * The index on "lastName" should not evaluate the condition on "firstName", + * even thought it will be set in the filter. + */ +public interface QueryIndex { + + /** + * Estimate the cost to query with the given filter. The returned + * cost is a value between 1 (very fast; lookup of a unique node) and the + * estimated number of nodes to traverse. + * + * @param filter the filter + * @param root root state of the current repository snapshot + * @return the estimated cost in number of read nodes + */ + double getCost(Filter filter, NodeState root); + + /** + * Start a query. + * + * @param filter the filter + * @param root root state of the current repository snapshot + * @return a cursor to iterate over the result + */ + Cursor query(Filter filter, NodeState root); + + /** + * Get the query plan for the given filter. + * + * @param filter the filter + * @param root root state of the current repository snapshot + * @return the query plan + */ + String getPlan(Filter filter, NodeState root); + + /** + * Get the unique index name. + * + * @return the index name + */ + String getIndexName(); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryIndexProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryIndexProvider.java new file mode 100644 index 00000000000..0a967470c72 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryIndexProvider.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.spi.query; + +import java.util.List; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * A mechanism to index data. Indexes might be added or removed at runtime, + * possibly by changing content in the repository. The provider knows about the + * indexes available at a given time. + */ +public interface QueryIndexProvider { + + /** + * Get the currently configured indexes. + * + * @return the list of indexes + */ + @Nonnull + List getQueryIndexes(NodeState nodeState); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/ConfigurationParameters.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/ConfigurationParameters.java new file mode 100644 index 00000000000..a0dbfcbca27 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/ConfigurationParameters.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.spi.security; + +import java.util.Collections; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ConfigurationParameters... TODO + */ +public class ConfigurationParameters { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(ConfigurationParameters.class); + + public static final ConfigurationParameters EMPTY = new ConfigurationParameters(); + + private final Map options; + + public ConfigurationParameters() { + this(null); + } + + public ConfigurationParameters(Map options) { + this.options = (options == null) ? Collections.emptyMap() : Collections.unmodifiableMap(options); + } + + public T getConfigValue(String key, T defaultValue) { + if (options != null && options.containsKey(key)) { + return convert(options.get(key), defaultValue); + } else { + return defaultValue; + } + } + + //--------------------------------------------------------< private >--- + @SuppressWarnings("unchecked") + private static T convert(Object configProperty, T defaultValue) { + T value; + String str = configProperty.toString(); + Class targetClass = (defaultValue == null) ? configProperty.getClass() : defaultValue.getClass(); + try { + if (targetClass == configProperty.getClass()) { + value = (T) configProperty; + } else if (targetClass == String.class) { + value = (T) str; + } else if (targetClass == Integer.class) { + value = (T) Integer.valueOf(str); + } else if (targetClass == Long.class) { + value = (T) Long.valueOf(str); + } else if (targetClass == Double.class) { + value = (T) Double.valueOf(str); + } else if (targetClass == Boolean.class) { + value = (T) Boolean.valueOf(str); + } else { + // unsupported target type + log.warn("Unsupported target type {} for value {}", targetClass.getName(), str); + throw new IllegalArgumentException("Cannot convert config entry " + str + " to " + targetClass.getName()); + } + } catch (NumberFormatException e) { + log.warn("Invalid value {}; cannot be parsed into {}", str, targetClass.getName()); + value = defaultValue; + } + return value; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/OpenSecurityProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/OpenSecurityProvider.java new file mode 100644 index 00000000000..20a60b793b1 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/OpenSecurityProvider.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security; + +import java.util.Collections; +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.apache.jackrabbit.oak.spi.security.authentication.LoginContextProvider; +import org.apache.jackrabbit.oak.spi.security.authentication.OpenLoginContextProvider; +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenProvider; +import org.apache.jackrabbit.oak.spi.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.oak.spi.security.authorization.OpenAccessControlProvider; +import org.apache.jackrabbit.oak.spi.security.principal.PrincipalConfiguration; +import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConfiguration; +import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration; +import org.apache.jackrabbit.oak.spi.state.NodeStore; + +/** + * OpenSecurityProvider... TODO: review if we really have the need for that once TODO in InitialContent is resolved + */ +public class OpenSecurityProvider implements SecurityProvider { + + @Nonnull + @Override + public Iterable getSecurityConfigurations() { + return Collections.singletonList(getAccessControlProvider()); + } + + @Nonnull + @Override + public LoginContextProvider getLoginContextProvider(NodeStore nodeStore, QueryIndexProvider indexProvider) { + return new OpenLoginContextProvider(); + } + + @Nonnull + @Override + public TokenProvider getTokenProvider(Root root) { + throw new UnsupportedOperationException(); + } + + @Nonnull + @Override + public AccessControlProvider getAccessControlProvider() { + return new OpenAccessControlProvider(); + } + + @Nonnull + @Override + public PrivilegeConfiguration getPrivilegeConfiguration() { + throw new UnsupportedOperationException(); + } + + @Nonnull + @Override + public UserConfiguration getUserConfiguration() { + throw new UnsupportedOperationException(); + } + + @Nonnull + @Override + public PrincipalConfiguration getPrincipalConfiguration() { + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/SecurityConfiguration.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/SecurityConfiguration.java new file mode 100644 index 00000000000..3f71f8f6223 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/SecurityConfiguration.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.spi.security; + +import java.util.Collections; +import java.util.List; +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.spi.commit.Observer; +import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; +import org.apache.jackrabbit.oak.spi.xml.ProtectedItemImporter; + +/** + * PluginConfiguration... TODO + */ +public interface SecurityConfiguration { + + @Nonnull + ConfigurationParameters getConfigurationParameters(); + + @Nonnull + List getValidatorProviders(); + + @Nonnull + List getCommitObservers(); + + @Nonnull + List getProtectedItemImporters(); + + /** + * Default implementation that provides empty validators/parameters. + */ + public static class Default implements SecurityConfiguration { + + @Nonnull + @Override + public ConfigurationParameters getConfigurationParameters() { + return ConfigurationParameters.EMPTY; + } + + @Nonnull + @Override + public List getValidatorProviders() { + return Collections.emptyList(); + } + + @Nonnull + @Override + public List getCommitObservers() { + return Collections.emptyList(); + } + + @Nonnull + @Override + public List getProtectedItemImporters() { + return Collections.emptyList(); + } + } + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/SecurityProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/SecurityProvider.java new file mode 100644 index 00000000000..b2edefbfb63 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/SecurityProvider.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.spi.security; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.apache.jackrabbit.oak.spi.security.authentication.LoginContextProvider; +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenProvider; +import org.apache.jackrabbit.oak.spi.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.oak.spi.security.principal.PrincipalConfiguration; +import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConfiguration; +import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration; +import org.apache.jackrabbit.oak.spi.state.NodeStore; + +/** + * SecurityProvider... TODO + */ +public interface SecurityProvider { + + @Nonnull + Iterable getSecurityConfigurations(); + + // TODO review again + @Nonnull + LoginContextProvider getLoginContextProvider(NodeStore nodeStore, QueryIndexProvider indexProvider); + + @Nonnull + TokenProvider getTokenProvider(Root root); + + @Nonnull + AccessControlProvider getAccessControlProvider(); + + @Nonnull + PrivilegeConfiguration getPrivilegeConfiguration(); + + @Nonnull + UserConfiguration getUserConfiguration(); + + @Nonnull + PrincipalConfiguration getPrincipalConfiguration(); +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/AbstractLoginModule.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/AbstractLoginModule.java new file mode 100644 index 00000000000..d76c3bd91fb --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/AbstractLoginModule.java @@ -0,0 +1,416 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.authentication; + +import java.io.IOException; +import java.security.Principal; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.jcr.Credentials; +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +import org.apache.jackrabbit.oak.spi.security.authentication.callback.CredentialsCallback; +import org.apache.jackrabbit.oak.spi.security.authentication.callback.PrincipalProviderCallback; +import org.apache.jackrabbit.oak.spi.security.authentication.callback.RepositoryCallback; +import org.apache.jackrabbit.oak.spi.security.authentication.callback.SecurityProviderCallback; +import org.apache.jackrabbit.oak.spi.security.authentication.callback.UserManagerCallback; +import org.apache.jackrabbit.oak.spi.security.principal.PrincipalProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Abstract implementation of the {@link LoginModule} interface that can act + * as base class for login modules that aim to autenticate subjects against + * information stored in the content repository. + * + *

    LoginModule Methods

    + * This base class provides a simple implementation for the following methods + * of the {@code LoginModule} interface: + * + *
      + *
    • {@link LoginModule#initialize(Subject, CallbackHandler, Map, Map) Initialize}: + * Initialization of this abstract module sets the following protected instance + * fields: + *
        + *
      • subject: The subject to be authenticated,
      • + *
      • callbackHandler: The callback handler passed to the login module,
      • + *
      • shareState: The map used to share state information with other login modules,
      • + *
      • options: The configuration options of this login module as specified + * in the {@link javax.security.auth.login.Configuration}.
      • + *
      + *
    • + *
    • {@link LoginModule#logout() Logout}: + * If the authenticated subject is not empty this logout implementation + * attempts to clear both principals and public credentials and returns + * {@code true}.
    • + *
    • {@link LoginModule#abort() Abort}: Clears the state of this login + * module by setting all private instance variables created in phase 1 or 2 + * to {@code null}. Subclasses are in charge of releasing their own state + * information by either overriding {@link #clearState()}.
    • + *
    + * + *

    Utility Methods

    + * The following methods are provided in addition: + * + *
      + *
    • {@link #clearState()}: Clears all private state information that has + * be created during login. This method in called in {@link #abort()} and + * subclasses are expected to override this method.
    • + * + *
    • {@link #getSupportedCredentials()}: Abstract method used by + * {@link #getCredentials()} that reveals which credential implementations + * are supported by the {@code LoginModule}.
    • + * + *
    • {@link #getCredentials()}: Tries to retrieve valid (supported) + * Credentials in the following order: + *
        + *
      1. using a {@link CredentialsCallback},
      2. + *
      3. looking for a {@link #SHARED_KEY_CREDENTIALS} entry in the shared + * state (see also {@link #getSharedCredentials()} and finally by
      4. + *
      5. searching for valid credentials in the subject.
      6. + *
    • + * + *
    • {@link #getSharedCredentials()}: This method returns credentials + * passed to the login module with the share state. The key to share credentials + * with a another module extending from this base class is + * {@link #SHARED_KEY_CREDENTIALS}. Note, that this method does not verify + * if the credentials provided by the shared state are + * {@link #getSupportedCredentials() supported}.
    • + * + *
    • {@link #getSharedLoginName()}: If the shared state contains an entry + * for {@link #SHARED_KEY_LOGIN_NAME} this method returns the value as login name.
    • + * + *
    • {@link #getSecurityProvider()}: Returns the configured security + * provider or {@code null}.
    • + * + *
    • {@link #getRoot()}: Provides access to the latest state of the + * repository in order to retrieve user or principal information required to + * authenticate the subject as well as to write back information during + * {@link #commit()}.
    • + * + *
    • {@link #getUserManager()}: Returns an instance of the configured + * {@link UserManager} or {@code null}.
    • + * + *
    • {@link #getPrincipalProvider()}: Returns an instance of the configured + * principal provider or {@code null}.
    • + * + *
    • {@link #getPrincipals(String)}: Utility that returns all principals + * associated with a given user id. This method might be be called after + * successful authentication in order to be able to populate the subject + * during {@link #commit()}. The implementation is a shortcut for calling + * {@link PrincipalProvider#getPrincipals(String) getPrincipals(String userId} + * on the provider exposed by {@link #getPrincipalProvider()}
    • + *
    + */ +public abstract class AbstractLoginModule implements LoginModule { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(AbstractLoginModule.class); + + /** + * Key of the sharedState entry referring to validated Credentials that is + * shared between multiple login modules. + */ + public static final String SHARED_KEY_CREDENTIALS = "org.apache.jackrabbit.credentials"; + + /** + * Key of the sharedState entry referring to a valid login ID that is shared + * between multiple login modules. + */ + public static final String SHARED_KEY_LOGIN_NAME = "javax.security.auth.login.name"; + + protected Subject subject; + protected CallbackHandler callbackHandler; + protected Map sharedState; + protected ConfigurationParameters options; + + private SecurityProvider securityProvider; + private Root root; + + //--------------------------------------------------------< LoginModule >--- + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { + this.subject = subject; + this.callbackHandler = callbackHandler; + this.sharedState = sharedState; + this.options = new ConfigurationParameters(options); + } + + @Override + public boolean logout() throws LoginException { + boolean success = false; + if (!subject.getPrincipals().isEmpty() && !subject.getPublicCredentials(Credentials.class).isEmpty()) { + // clear subject if not readonly + if (!subject.isReadOnly()) { + subject.getPrincipals().clear(); + subject.getPublicCredentials().clear(); + } + success = true; + } + return success; + } + + @Override + public boolean abort() throws LoginException { + clearState(); + return true; + } + + //-------------------------------------------------------------------------- + /** + * Clear state information that has been created during {@link #login()}. + */ + protected void clearState() { + securityProvider = null; + root = null; + } + + /** + * @return A set of supported credential classes. + */ + @Nonnull + protected abstract Set getSupportedCredentials(); + + /** + * Tries to retrieve valid (supported) Credentials: + *
      + *
    1. using a {@link CredentialsCallback},
    2. + *
    3. looking for a {@link #SHARED_KEY_CREDENTIALS} entry in the + * shared state (see also {@link #getSharedCredentials()} and finally by
    4. + *
    5. searching for valid credentials in the subject.
    6. + *
    + * + * @return Valid (supported) credentials or {@code null}. + */ + @CheckForNull + protected Credentials getCredentials() { + Set supported = getSupportedCredentials(); + if (callbackHandler != null) { + log.debug("Login: retrieving Credentials using callback."); + try { + CredentialsCallback callback = new CredentialsCallback(); + callbackHandler.handle(new Callback[]{callback}); + Credentials creds = callback.getCredentials(); + if (creds != null && supported.contains(creds.getClass())) { + log.debug("Login: Credentials '{}' obtained from callback", creds); + return creds; + } else { + log.debug("Login: No supported credentials obtained from callback; trying shared state."); + } + } catch (UnsupportedCallbackException e) { + log.warn(e.getMessage()); + } catch (IOException e) { + log.error(e.getMessage()); + } + } + + Credentials creds = getSharedCredentials(); + if (creds != null && supported.contains(creds.getClass())) { + log.debug("Login: Credentials obtained from shared state."); + return creds; + } else { + log.debug("Login: No supported credentials found in shared state; looking for credentials in subject."); + for (Class clz : getSupportedCredentials()) { + Set cds = subject.getPublicCredentials(clz); + if (!cds.isEmpty()) { + log.debug("Login: Credentials found in subject."); + return cds.iterator().next(); + } + } + } + + log.debug("No credentials found."); + return null; + } + + /** + * @return The credentials passed to this login module with the shared state. + * @see #SHARED_KEY_CREDENTIALS + */ + @CheckForNull + protected Credentials getSharedCredentials() { + Credentials shared = null; + if (sharedState.containsKey(SHARED_KEY_CREDENTIALS)) { + Object sc = sharedState.get(SHARED_KEY_CREDENTIALS); + if (sc instanceof Credentials) { + shared = (Credentials) sc; + } else { + log.debug("Login: Invalid value for share state entry " + SHARED_KEY_CREDENTIALS + ". Credentials expected."); + } + } + + return shared; + } + + /** + * @return The login name passed to this login module with the shared state. + * @see #SHARED_KEY_LOGIN_NAME + */ + @CheckForNull + protected String getSharedLoginName() { + if (sharedState.containsKey(SHARED_KEY_LOGIN_NAME)) { + return sharedState.get(SHARED_KEY_LOGIN_NAME).toString(); + } else { + return null; + } + } + + /** + * Tries to obtain the {@code SecurityProvider} object from the callback + * handler using a new SecurityProviderCallback and keeps the value as + * private field. If the callback handler isn't able to handle the + * SecurityProviderCallback this method returns {@code null}. + * + * @return The {@code SecurityProvider} associated with this + * {@code LoginModule} or {@code null}. + */ + @CheckForNull + protected SecurityProvider getSecurityProvider() { + if (securityProvider == null && callbackHandler != null) { + SecurityProviderCallback scb = new SecurityProviderCallback(); + try { + callbackHandler.handle(new Callback[] {scb}); + securityProvider = scb.getSecurityProvider(); + } catch (UnsupportedCallbackException e) { + log.debug(e.getMessage()); + } catch (IOException e) { + log.debug(e.getMessage()); + } + } + return securityProvider; + } + + /** + * Tries to obtain a {@code Root} object from the callback handler using + * a new RepositoryCallback and keeps the value as private field. + * If the callback handler isn't able to handle the RepositoryCallback + * this method returns {@code null}. + * + * @return The {@code Root} associated with this {@code LoginModule} or + * {@code null}. + */ + @CheckForNull + protected Root getRoot() { + if (root == null && callbackHandler != null) { + RepositoryCallback rcb = new RepositoryCallback(); + try { + callbackHandler.handle(new Callback[] {rcb}); + root = rcb.getRoot(); + } catch (UnsupportedCallbackException e) { + log.debug(e.getMessage()); + } catch (IOException e) { + log.debug(e.getMessage()); + } + } + return root; + } + + /** + * Retrieves the {@link UserManager} that should be used to handle + * this authentication. If no user manager has been configure this + * method returns {@code null}. + * + * @return A instance of {@code UserManager} or {@code null}. + */ + @CheckForNull + protected UserManager getUserManager() { + UserManager userManager = null; + SecurityProvider sp = getSecurityProvider(); + Root root = getRoot(); + if (root != null && sp != null) { + userManager = sp.getUserConfiguration().getUserManager(root, NamePathMapper.DEFAULT); + } + + if (userManager == null && callbackHandler != null) { + try { + UserManagerCallback userCallBack = new UserManagerCallback(); + callbackHandler.handle(new Callback[] {userCallBack}); + userManager = userCallBack.getUserManager(); + } catch (IOException e) { + log.debug(e.getMessage()); + } catch (UnsupportedCallbackException e) { + log.debug(e.getMessage()); + } + } + + return userManager; + } + + /** + * Retrieves the {@link PrincipalProvider} that should be used to handle + * this authentication. If no principal provider has been configure this + * method returns {@code null}. + * + * @return A instance of {@code PrincipalProvider} or {@code null}. + */ + @CheckForNull + protected PrincipalProvider getPrincipalProvider() { + PrincipalProvider principalProvider = null; + SecurityProvider sp = getSecurityProvider(); + Root root = getRoot(); + if (root != null && sp != null) { + principalProvider = sp.getPrincipalConfiguration().getPrincipalProvider(root, NamePathMapper.DEFAULT); + } + + if (principalProvider == null && callbackHandler != null) { + try { + PrincipalProviderCallback principalCallBack = new PrincipalProviderCallback(); + callbackHandler.handle(new Callback[] {principalCallBack}); + principalProvider = principalCallBack.getPrincipalProvider(); + } catch (IOException e) { + log.debug(e.getMessage()); + } catch (UnsupportedCallbackException e) { + log.debug(e.getMessage()); + } + } + return principalProvider; + } + + /** + * Retrieves all principals associated with the specified {@code userId} for + * the configured principal provider. + * + * @param userId The id of the user. + * @return The set of principals associated with the given {@code userId}. + * @see #getPrincipalProvider() + */ + @Nonnull + protected Set getPrincipals(String userId) { + PrincipalProvider principalProvider = getPrincipalProvider(); + if (principalProvider == null) { + log.debug("Cannot retrieve principals. No principal provider configured."); + return Collections.emptySet(); + } else { + return principalProvider.getPrincipals(userId); + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/Authentication.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/Authentication.java new file mode 100644 index 00000000000..e29ab39b1dd --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/Authentication.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.spi.security.authentication; + +import javax.jcr.Credentials; +import javax.security.auth.login.LoginException; + +/** + * The {@code Authentication} interface defines methods to validate + * {@link javax.jcr.Credentials Credentials} during the + * {@link javax.security.auth.spi.LoginModule#login() login step} of the + * authentication process. The validation depends on the authentication + * mechanism in place.

    + * + * A given implementation may only handle certain types of {@code Credentials} + * as the authentication process is tightly coupled to the semantics of the + * {@code Credentials}.

    + * + * For example a implementation may only be able to validate UserID/password + * pairs such as passed with {@link javax.jcr.SimpleCredentials}, while another + * might be responsible for validating login token issued by the repository or + * an external access token generation mechanism. + */ +public interface Authentication { + + /** + * Validates the specified {@code Credentials} and returns {@code true} if + * the validation was successful. + * + * @param credentials to verify + * @return {@code true} if the validation was successful; {@code false} + * if the specified credentials are not supported and this authentication + * implementation cannot verify their validity. + * @throws LoginException if the authentication failed. + */ + boolean authenticate(Credentials credentials) throws LoginException; +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/GuestLoginModule.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/GuestLoginModule.java new file mode 100644 index 00000000000..a395fbe559b --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/GuestLoginModule.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.authentication; + +import java.io.IOException; +import java.util.Map; +import javax.jcr.Credentials; +import javax.jcr.GuestCredentials; +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.spi.LoginModule; + +import org.apache.jackrabbit.oak.spi.security.authentication.callback.CredentialsCallback; +import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@code GuestLoginModule} is intended to provide backwards compatibility + * with the login handling present in the JCR reference implementation located + * in jackrabbit-core. While the specification claims that {@link javax.jcr.Repository#login} + * with {@code null} Credentials implies that the authentication process is + * handled externally, the default implementation jackrabbit-core treated it + * as 'anonymous' login such as covered by using {@link GuestCredentials}.

    + * + * This {@code LoginModule} implementation performs the following tasks upon + * {@link #login()}. + * + *

      + *
    1. Try to retrieve JCR credentials from the {@link CallbackHandler} using + * the {@link CredentialsCallback}
    2. + *
    3. In case no credentials could be obtained it pushes a new instance of + * {@link GuestCredentials} to the shared stated. Subsequent login modules + * in the authentication process may retrieve the {@link GuestCredentials} + * instead of failing to obtain any credentials.
    4. + *
    + * + * If this login module pushed {@link GuestLoginModule} to the shared state + * in phase 1 it will add those credentials and the {@link EveryonePrincipal} + * to the subject in phase 2 of the login process. Subsequent login modules + * my choose to provide additional principals/credentials associated with + * a guest login.

    + * + * The authentication configuration using this {@code LoginModule} could for + * example look as follows: + * + *

    + *
    + *    jackrabbit.oak {
    + *            org.apache.jackrabbit.oak.spi.security.authentication.GuestLoginModule  optional;
    + *            org.apache.jackrabbit.oak.security.authentication.user.LoginModuleImpl required;
    + *    };
    + *
    + * 
    + * + * In this case calling {@link javax.jcr.Repository#login()} would be equivalent + * to {@link javax.jcr.Repository#login(javax.jcr.Credentials) repository.login(new GuestCredentials()}. + */ +public final class GuestLoginModule implements LoginModule { + + private static final Logger log = LoggerFactory.getLogger(GuestLoginModule.class); + + private Subject subject; + private CallbackHandler callbackHandler; + private Map sharedState; + + private GuestCredentials guestCredentials; + + //--------------------------------------------------------< LoginModule >--- + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { + this.subject = subject; + this.callbackHandler = callbackHandler; + this.sharedState = sharedState; + } + + @Override + public boolean login() { + if (callbackHandler != null) { + CredentialsCallback ccb = new CredentialsCallback(); + try { + callbackHandler.handle(new Callback[] {ccb}); + Credentials credentials = ccb.getCredentials(); + if (credentials == null) { + guestCredentials = new GuestCredentials(); + sharedState.put(AbstractLoginModule.SHARED_KEY_CREDENTIALS, guestCredentials); + return true; + } + } catch (IOException e) { + log.debug("Login: Failed to retrieve Credentials from CallbackHandler", e); + } catch (UnsupportedCallbackException e) { + log.debug("Login: Failed to retrieve Credentials from CallbackHandler", e); + } + } + + // ignore this login module + return false; + } + + @Override + public boolean commit() { + if (authenticationSucceeded()) { + if (!subject.isReadOnly()) { + subject.getPublicCredentials().add(guestCredentials); + subject.getPrincipals().add(EveryonePrincipal.getInstance()); + } + return true; + } else { + return false; + } + } + + @Override + public boolean abort() { + guestCredentials = null; + return true; + } + + @Override + public boolean logout() { + return authenticationSucceeded(); + } + + private boolean authenticationSucceeded() { + return guestCredentials != null; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/ImpersonationCredentials.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/ImpersonationCredentials.java new file mode 100644 index 00000000000..bbb573ad665 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/ImpersonationCredentials.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.spi.security.authentication; + +import org.apache.jackrabbit.oak.api.AuthInfo; + +import javax.jcr.Credentials; + +/** + * Implementation of the JCR {@code Credentials} interface used to distinguish + * a regular login request from {@link javax.jcr.Session#impersonate(javax.jcr.Credentials)}. + */ +public class ImpersonationCredentials implements Credentials { + + private final Credentials baseCredentials; + private final AuthInfo authInfo; + + public ImpersonationCredentials(Credentials baseCredentials, AuthInfo authInfo) { + this.baseCredentials = baseCredentials; + this.authInfo = authInfo; + } + + /** + * Returns the {@code Credentials} originally passed to + * {@link javax.jcr.Session#impersonate(javax.jcr.Credentials)}. + * + * @return the {@code Credentials} originally passed to + * {@link javax.jcr.Session#impersonate(javax.jcr.Credentials)}. + */ + public Credentials getBaseCredentials() { + return baseCredentials; + } + + /** + * Returns the {@code AuthInfo} present with the editing session that want + * to impersonate. + * + * @return {@code AuthInfo} present with the editing session that want + * to impersonate. + * @see org.apache.jackrabbit.oak.api.ContentSession#getAuthInfo() + */ + public AuthInfo getImpersonatorInfo() { + return authInfo; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/JaasLoginContext.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/JaasLoginContext.java new file mode 100644 index 00000000000..c2699ea0832 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/JaasLoginContext.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.spi.security.authentication; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginException; + +/** + * Bridge class that connects the JAAS {@link javax.security.auth.login.LoginContext} class with the + * {@link LoginContext} interface used by Oak. + */ +public class JaasLoginContext extends javax.security.auth.login.LoginContext implements LoginContext { + + public JaasLoginContext(String name) throws LoginException { + super(name); + } + + public JaasLoginContext(String name, Subject subject) throws LoginException { + super(name, subject); + } + + public JaasLoginContext(String name, CallbackHandler handler) throws LoginException { + super(name, handler); + } + + public JaasLoginContext(String name, Subject subject, CallbackHandler handler) + throws LoginException { + super(name, subject, handler); + } + + public JaasLoginContext(String name, Subject subject, CallbackHandler handler, + Configuration configuration) throws LoginException { + super(name, subject, handler, configuration); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/LoginContext.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/LoginContext.java new file mode 100644 index 00000000000..7dc7d3f1a68 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/LoginContext.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.spi.security.authentication; + +import javax.security.auth.Subject; +import javax.security.auth.login.LoginException; + +/** + * Interface version of the JAAS {@link javax.security.auth.login.LoginContext} + * class. It is used to make integration of non-JAAS authentication components + * easier while still retaining full JAAS support. The {@link JaasLoginContext} + * class acts as a bridge that connects the JAAS + * {@link javax.security.auth.login.LoginContext} class with this interface. + */ +public interface LoginContext { + + /** + * @see javax.security.auth.login.LoginContext#getSubject() + */ + Subject getSubject(); + + /** + * @see javax.security.auth.login.LoginContext#login() + */ + void login() throws LoginException; + + /** + * @see javax.security.auth.login.LoginContext#logout() + */ + void logout() throws LoginException; + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/LoginContextProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/LoginContextProvider.java new file mode 100644 index 00000000000..e134fd0747e --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/LoginContextProvider.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.spi.security.authentication; + +import javax.annotation.Nonnull; +import javax.jcr.Credentials; +import javax.security.auth.login.LoginException; + +/** + * Configurable provider taking care of building login contexts for + * the desired authentication mechanism. + *

    + * This provider defines a single method {@link #getLoginContext(javax.jcr.Credentials, String)} + * that takes the {@link Credentials credentials} and the workspace name such + * as passed to {@link org.apache.jackrabbit.oak.api.ContentRepository#login(javax.jcr.Credentials, String)}. + */ +public interface LoginContextProvider { + + /** + * Returns a new login context instance for handling authentication. + * + * @param credentials The {@link Credentials} such as passed to the + * {@link org.apache.jackrabbit.oak.api.ContentRepository#login(javax.jcr.Credentials, String) login} + * method of the repository. + * @param workspaceName The name of the workspace that is being accessed by + * the login called. + * @return a new login context + * @throws LoginException If an error occurs while creating a new context. + */ + @Nonnull + LoginContext getLoginContext(Credentials credentials, String workspaceName) throws LoginException; +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/OpenLoginContextProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/OpenLoginContextProvider.java new file mode 100644 index 00000000000..c06aeed28a0 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/OpenLoginContextProvider.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.spi.security.authentication; + +import javax.annotation.Nonnull; +import javax.jcr.Credentials; +import javax.security.auth.Subject; + +/** + * This class provides login contexts that accept any credentials and doesn't + * validate specified workspace name. + */ +public class OpenLoginContextProvider implements LoginContextProvider { + + @Override + @Nonnull + public LoginContext getLoginContext(final Credentials credentials, String workspaceName) { + return new LoginContext() { + @Override + public Subject getSubject() { + Subject subject = new Subject(); + if (credentials != null) { + subject.getPrivateCredentials().add(credentials); + } + subject.setReadOnly(); + return subject; + } + @Override + public void login() { + // do nothing + } + @Override + public void logout() { + // do nothing + } + }; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/callback/CredentialsCallback.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/callback/CredentialsCallback.java new file mode 100644 index 00000000000..e4ad64d9b1e --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/callback/CredentialsCallback.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.spi.security.authentication.callback; + +import javax.annotation.CheckForNull; +import javax.jcr.Credentials; +import javax.security.auth.callback.Callback; + +/** + * Callback implementation to retrieve {@code Credentials}. + */ +public class CredentialsCallback implements Callback { + + private Credentials credentials; + + /** + * Returns the {@link Credentials} that have been set before using + * {@link #setCredentials(javax.jcr.Credentials)}. + * + * @return The {@link Credentials} to be used for authentication or {@code null}. + */ + @CheckForNull + public Credentials getCredentials() { + return credentials; + } + + /** + * Set the credentials. + * + * @param credentials The credentials to be used in the authentication + * process. They may be null if no credentials have been specified in + * {@link org.apache.jackrabbit.oak.api.ContentRepository#login(javax.jcr.Credentials, String)} + */ + public void setCredentials(Credentials credentials) { + this.credentials = credentials; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/callback/PrincipalProviderCallback.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/callback/PrincipalProviderCallback.java new file mode 100644 index 00000000000..95186df4e28 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/callback/PrincipalProviderCallback.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.spi.security.authentication.callback; + +import javax.security.auth.callback.Callback; + +import org.apache.jackrabbit.oak.spi.security.principal.PrincipalProvider; + +/** + * Callback implementation used to pass a {@link PrincipalProvider} to the + * login module. + */ +public class PrincipalProviderCallback implements Callback { + + private PrincipalProvider principalProvider; + + /** + * Returns the principal provider as set using + * {@link #setPrincipalProvider(org.apache.jackrabbit.oak.spi.security.principal.PrincipalProvider)} + * or {@code null}. + * + * @return an instance of {@code PrincipalProvider} or {@code null} if no + * provider has been set before. + */ + public PrincipalProvider getPrincipalProvider() { + return principalProvider; + } + + /** + * Sets the {@code PrincipalProvider} that is being used during the + * authentication process. + * + * @param principalProvider The principal provider to use during the + * authentication process. + */ + public void setPrincipalProvider(PrincipalProvider principalProvider) { + this.principalProvider = principalProvider; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/callback/RepositoryCallback.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/callback/RepositoryCallback.java new file mode 100644 index 00000000000..79190c3b120 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/callback/RepositoryCallback.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.spi.security.authentication.callback; + +import java.util.Collections; +import javax.annotation.CheckForNull; +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.core.RootImpl; +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.apache.jackrabbit.oak.spi.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.oak.spi.security.authorization.OpenAccessControlProvider; +import org.apache.jackrabbit.oak.spi.security.principal.SystemPrincipal; +import org.apache.jackrabbit.oak.spi.state.NodeStore; + +/** + * Callback implementation used to access the repository. It allows to set and + * get the {@code NodeStore} and the name of the workspace for which the login + * applies. In addition it provides access to a {@link Root} object based on + * the given node store and workspace name. + */ +public class RepositoryCallback implements Callback { + + private NodeStore nodeStore; + private QueryIndexProvider indexProvider; + private String workspaceName; + + public String getWorkspaceName() { + return workspaceName; + } + + @CheckForNull + public Root getRoot() { + if (nodeStore != null) { + Subject subject = new Subject(true, Collections.singleton(SystemPrincipal.INSTANCE), Collections.emptySet(), Collections.emptySet()); + AccessControlProvider acProvider = new OpenAccessControlProvider(); + return new RootImpl(nodeStore, workspaceName, subject, acProvider, indexProvider); + } + return null; + } + + public void setNodeStore(NodeStore nodeStore) { + this.nodeStore = nodeStore; + } + + public void setIndexProvider(QueryIndexProvider indexProvider) { + this.indexProvider = indexProvider; + } + + public void setWorkspaceName(String workspaceName) { + this.workspaceName = workspaceName; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/callback/SecurityProviderCallback.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/callback/SecurityProviderCallback.java new file mode 100644 index 00000000000..8ffd10d543b --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/callback/SecurityProviderCallback.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.authentication.callback; + +import javax.annotation.CheckForNull; +import javax.security.auth.callback.Callback; + +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; + +/** + * Callback implementation to set and get the {@link SecurityProvider}. + */ +public class SecurityProviderCallback implements Callback { + + private SecurityProvider securityProvider; + + @CheckForNull + public SecurityProvider getSecurityProvider() { + return securityProvider; + } + + public void setSecurityProvider(SecurityProvider securityProvider) { + this.securityProvider = securityProvider; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/callback/TokenProviderCallback.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/callback/TokenProviderCallback.java new file mode 100644 index 00000000000..e0fc2c349b9 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/callback/TokenProviderCallback.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.spi.security.authentication.callback; + +import javax.security.auth.callback.Callback; + +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenProvider; + +/** + * Callback implementation to set and retrieve a login token provider. + */ +public class TokenProviderCallback implements Callback { + + private TokenProvider tokenProvider; + + /** + * Returns the principal provider as set using + * {@link #setTokenProvider(TokenProvider)} + * or {@code null}. + * + * @return an instance of {@code PrincipalProvider} or {@code null} if no + * provider has been set before. + */ + public TokenProvider getTokenProvider() { + return tokenProvider; + } + + /** + * Sets the {@code TokenProvider} that is being used during the + * authentication process. + * + * @param tokenProvider The {@code TokenProvider} to use during the + * authentication process. + */ + public void setTokenProvider(TokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/callback/UserManagerCallback.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/callback/UserManagerCallback.java new file mode 100644 index 00000000000..6f000705c00 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/callback/UserManagerCallback.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.spi.security.authentication.callback; + +import javax.security.auth.callback.Callback; + +import org.apache.jackrabbit.api.security.user.UserManager; + +/** + * Callback implementation used to pass a {@link UserManager} to the + * login module. + */ +public class UserManagerCallback implements Callback { + + private UserManager userManager; + + /** + * Returns the user provider as set using + * {@link #setUserManager(org.apache.jackrabbit.api.security.user.UserManager)} + * or {@code null}. + * + * @return an instance of {@code UserManager} or {@code null} if no + * provider has been set before. + */ + public UserManager getUserManager() { + return userManager; + } + + /** + * Sets the {@code UserManager} that is being used during the + * authentication process. + * + * @param userManager The user provider to use during the + * authentication process. + */ + public void setUserManager(UserManager userManager) { + this.userManager = userManager; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/DefaultSyncHandler.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/DefaultSyncHandler.java new file mode 100644 index 00000000000..b59ccc8b1a3 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/DefaultSyncHandler.java @@ -0,0 +1,180 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.authentication.external; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import javax.annotation.CheckForNull; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.plugins.value.ValueFactoryImpl; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * DefaultSyncHandler... TODO + */ +public class DefaultSyncHandler implements SyncHandler { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(DefaultSyncHandler.class); + + private UserManager userManager; + private Root root; + private SyncMode mode; + private ConfigurationParameters options; + + private ValueFactory valueFactory; + + private boolean initialized; + + @Override + public boolean initialize(UserManager userManager, Root root, SyncMode mode, + ConfigurationParameters options) throws SyncException { + if (userManager == null || root == null) { + throw new SyncException("Error while initializing sync handler."); + } + this.userManager = userManager; + this.root = root; + this.mode = mode; + this.options = (options == null) ? ConfigurationParameters.EMPTY : options; + + valueFactory = new ValueFactoryImpl(root.getBlobFactory(), NamePathMapper.DEFAULT); + initialized = true; + return true; + } + + @Override + public boolean sync(ExternalUser externalUser) throws SyncException { + checkInitialized(); + try { + User user = getUser(externalUser); + if (user == null) { + createUser(externalUser); + } else { + updateUser(externalUser, user); + } + return true; + } catch (RepositoryException e) { + throw new SyncException(e); + } + } + + //-------------------------------------------------------------------------- + private void checkInitialized() { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + } + + @CheckForNull + private User getUser(ExternalUser externalUser) throws RepositoryException { + // TODO: deal with colliding authorizable that is group. + + Authorizable authorizable = userManager.getAuthorizable(externalUser.getId()); + if (authorizable == null) { + authorizable = userManager.getAuthorizable(externalUser.getPrincipal()); + } + + return (authorizable == null) ? null : (User) authorizable; + } + + @CheckForNull + private User createUser(ExternalUser externalUser) throws RepositoryException, SyncException { + if (mode.contains(SyncMode.MODE_CREATE_USER)) { + User user = userManager.createUser(externalUser.getId(), null, externalUser.getPrincipal(), externalUser.getPath()); + syncAuthorizable(externalUser, user); + return user; + } else { + return null; + } + } + + @CheckForNull + private Group createGroup(ExternalGroup externalGroup) throws RepositoryException, SyncException { + if (mode.contains(SyncMode.MODE_CREATE_GROUPS)) { + Group group = userManager.createGroup(externalGroup.getId(), externalGroup.getPrincipal(), externalGroup.getPath()); + syncAuthorizable(externalGroup, group); + return group; + } else { + return null; + } + } + + private void updateUser(ExternalUser externalUser, User user) throws RepositoryException, SyncException { + if (mode.contains(SyncMode.MODE_UPDATE)) { + syncAuthorizable(externalUser, user); + } + } + + private void syncAuthorizable(ExternalUser externalUser, Authorizable authorizable) throws RepositoryException, SyncException { + for (ExternalGroup externalGroup : externalUser.getGroups()) { + String groupId = externalGroup.getId(); + Group group; + Authorizable a = userManager.getAuthorizable(groupId); + if (a == null) { + group = createGroup(externalGroup); + } else { + group = (a.isGroup()) ? (Group) a : null; + } + + if (group != null) { + group.addMember(authorizable); + } else { + log.debug("No such group " + groupId + "; Ignoring group membership."); + } + } + + Map properties = externalUser.getProperties(); + for (String key : properties.keySet()) { + Object prop = properties.get(key); + if (prop instanceof Collection) { + Value[] values = createValues((Collection) prop); + authorizable.setProperty(key, values); + } else { + Value value = createValue(prop); + authorizable.setProperty(key, value); + } + } + } + + private Value createValue(Object propValue) { + // TODO + return null; + } + + private Value[] createValues(Collection propValues) { + List values = new ArrayList(); + for (Object obj : propValues) { + values.add(createValue(obj)); + } + return values.toArray(new Value[values.size()]); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalGroup.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalGroup.java new file mode 100644 index 00000000000..13b8b836010 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalGroup.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.authentication.external; + +/** + * ExternalGroup... TODO + */ +public interface ExternalGroup extends ExternalUser { +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModule.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModule.java new file mode 100644 index 00000000000..e637775f504 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModule.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.authentication.external; + +import java.util.Collections; +import java.util.Set; +import javax.jcr.SimpleCredentials; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.spi.security.authentication.AbstractLoginModule; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ExternalLoginModule... TODO + */ +public abstract class ExternalLoginModule extends AbstractLoginModule { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(ExternalLoginModule.class); + + public static final String PARAM_SYNC_MODE = "syncMode"; + public static final SyncMode DEFAULT_SYNC_MODE = SyncMode.DEFAULT_SYNC; + + private static final String PARAM_SYNC_HANDLER = "syncHandler"; + private static final String DEFAULT_SYNC_HANDLER = DefaultSyncHandler.class.getName(); + + //------------------------------------------------< ExternalLoginModule >--- + /** + * TODO + * + * @return + */ + protected abstract boolean loginSucceeded(); + + /** + * TODO + * + * @return + */ + protected abstract ExternalUser getExternalUser(); + + /** + * TODO + * + * @return + * @throws SyncException + */ + protected SyncHandler getSyncHandler() throws SyncException { + String shClass = options.getConfigValue(PARAM_SYNC_HANDLER, DEFAULT_SYNC_HANDLER); + Object syncHandler; + try { + // FIXME this will create problems within OSGi environment + syncHandler = Class.forName(shClass).newInstance(); + } catch (Exception e) { + throw new SyncException("Error while getting SyncHandler:", e); + } + + if (syncHandler instanceof SyncHandler) { + return (SyncHandler) syncHandler; + } else { + throw new SyncException("Invalid SyncHandler class configured: " + syncHandler.getClass().getName()); + } + } + + //------------------------------------------------< AbstractLoginModule >--- + + /** + * Default implementation of the {@link #getSupportedCredentials()} method + * that only lists {@link SimpleCredentials} as supported. Subclasses that + * wish to support other or additional credential implementations should + * override this method. + * + * @return An immutable set containing only the {@link SimpleCredentials} class. + */ + @Override + protected Set getSupportedCredentials() { + Class scClass = SimpleCredentials.class; + return Collections.singleton(scClass); + } + + //--------------------------------------------------------< LoginModule >--- + + /** + * TODO + * + * @return + * @throws LoginException + */ + @Override + public boolean commit() throws LoginException { + if (!loginSucceeded()) { + return false; + } + + try { + SyncHandler handler = getSyncHandler(); + Root root = getRoot(); + String smValue = options.getConfigValue(PARAM_SYNC_MODE, null); + SyncMode syncMode; + if (smValue == null) { + syncMode = DEFAULT_SYNC_MODE; + } else { + syncMode = SyncMode.fromStrings(Text.explode(smValue, ',', false)); + } + if (handler.initialize(getUserManager(), root, syncMode, options)) { + handler.sync(getExternalUser()); + root.commit(); + return true; + } else { + log.warn("Failed to initialize sync handler."); + return false; + } + } catch (SyncException e) { + throw new LoginException("User synchronization failed: " + e); + } catch (CommitFailedException e) { + throw new LoginException("User synchronization failed: " + e); + } + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalUser.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalUser.java new file mode 100644 index 00000000000..c1ca3e5a7be --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalUser.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.spi.security.authentication.external; + +import java.security.Principal; +import java.util.Map; +import java.util.Set; + +/** + * ExternalUser... TODO + */ +public interface ExternalUser { + + String getId(); + + Principal getPrincipal(); + + String getPath(); + + Set getGroups(); + + Map getProperties(); +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/EmptyIterator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncException.java similarity index 69% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/util/EmptyIterator.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncException.java index f7739a14050..479d26b72d4 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/EmptyIterator.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncException.java @@ -14,25 +14,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.util; - -import java.util.Iterator; -import java.util.NoSuchElementException; +package org.apache.jackrabbit.oak.spi.security.authentication.external; /** - * + * SyncException... TODO */ -public class EmptyIterator implements Iterator { +public class SyncException extends Exception { - public boolean hasNext() { - return false; + public SyncException(String s) { + super(s); } - public T next() { - throw new NoSuchElementException(); + public SyncException(Throwable throwable) { + super(throwable); } - public void remove() { - throw new UnsupportedOperationException(); + public SyncException(String s, Throwable throwable) { + super(s, throwable); } } \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncHandler.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncHandler.java new file mode 100644 index 00000000000..2fdf2704c91 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncHandler.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.spi.security.authentication.external; + +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; + +/** + * SyncHandler... TODO + */ +public interface SyncHandler { + + boolean initialize(UserManager userManager, Root root, SyncMode mode, + ConfigurationParameters options) throws SyncException; + + boolean sync(ExternalUser externalUser) throws SyncException; +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncMode.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncMode.java new file mode 100644 index 00000000000..68876cf2ff8 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncMode.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.authentication.external; + +/** + * SyncMode... TODO: define sync-modes + */ +public class SyncMode { + + public static final int MODE_NO_SYNC = 0; + public static final int MODE_CREATE_USER = 1; + public static final int MODE_CREATE_GROUPS = 2; + public static final int MODE_UPDATE = 4; + + public static final String CREATE_USER_NAME = "createUser"; + public static final String CREATE_GROUP_NAME = "createGroup"; + public static final String UPDATE = "update"; + + public static final SyncMode DEFAULT_SYNC = new SyncMode(MODE_CREATE_USER|MODE_CREATE_GROUPS|MODE_UPDATE); + + private final int mode; + + private SyncMode(int mode) { + this.mode = mode; + } + + public boolean contains(int mode) { + return (this.mode & mode) == mode; + } + + public static SyncMode fromString(String name) { + int mode; + if (CREATE_USER_NAME.equals(name)) { + mode = MODE_CREATE_USER; + } else if (CREATE_GROUP_NAME.equals(name)) { + mode = MODE_CREATE_GROUPS; + } else if (UPDATE.equals(name)) { + mode = MODE_UPDATE; + } else { + throw new IllegalArgumentException("invalid sync mode name " + name); + } + return fromInt(mode); + } + + public static SyncMode fromStrings(String[] names) { + int mode = MODE_NO_SYNC; + for (String name : names) { + mode |= fromString(name.trim()).mode; + } + return new SyncMode(mode); + } + + private static SyncMode fromInt(int mode) { + if (mode == DEFAULT_SYNC.mode) { + return DEFAULT_SYNC; + } + + if (mode < 0 || mode > DEFAULT_SYNC.mode) { + throw new IllegalArgumentException("invalid sync mode: " + mode); + } else { + return new SyncMode(mode); + } + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/token/TokenInfo.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/token/TokenInfo.java new file mode 100644 index 00000000000..3c562aea046 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/token/TokenInfo.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.authentication.token; + +import java.util.Map; +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; + +/** + * TokenInfo... TODO + */ +public interface TokenInfo { + + @Nonnull + String getUserId(); + + @Nonnull + String getToken(); + + boolean isExpired(long loginTime); + + boolean matches(TokenCredentials tokenCredentials); + + @Nonnull + Map getPrivateAttributes(); + + @Nonnull + Map getPublicAttributes(); +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/token/TokenProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/token/TokenProvider.java new file mode 100644 index 00000000000..7b4f6f52545 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/token/TokenProvider.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.spi.security.authentication.token; + +import java.util.Map; +import javax.annotation.CheckForNull; +import javax.jcr.Credentials; + +/** + * TokenProvider... TODO + */ +public interface TokenProvider { + + /** + * Optional configuration parameter to set the token expiration time in ms. + * Implementations that do not support this option will ignore any config + * options with that name. + */ + String PARAM_TOKEN_EXPIRATION = "tokenExpiration"; + + /** + * Optional configuration parameter to define the length of the key. + * Implementations that do not support this option will ignore any config + * options with that name. + */ + String PARAM_TOKEN_LENGTH = "tokenLength"; + + boolean doCreateToken(Credentials credentials); + + @CheckForNull + TokenInfo createToken(Credentials credentials); + + @CheckForNull + TokenInfo createToken(String userId, Map attributes); + + @CheckForNull + TokenInfo getTokenInfo(String token); + + boolean removeToken(TokenInfo tokenInfo); + + boolean resetTokenExpiration(TokenInfo tokenInfo, long loginTime); +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/AccessControlContext.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/AccessControlContext.java new file mode 100644 index 00000000000..457be1a3268 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/AccessControlContext.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.authorization; + +/** + * PermissionProvider... TODO + */ +public interface AccessControlContext { + + // TODO define how permissions eval is bound to a particular revision/branch. (passing Tree?) + CompiledPermissions getPermissions(); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/AccessControlProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/AccessControlProvider.java new file mode 100644 index 00000000000..ffe8aafc094 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/AccessControlProvider.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.spi.security.authorization; + +import javax.security.auth.Subject; + +import org.apache.jackrabbit.oak.spi.security.SecurityConfiguration; + +/** + * {@code AccessControlContextProvider}... + */ +public interface AccessControlProvider extends SecurityConfiguration { + + public AccessControlContext getAccessControlContext(Subject subject); +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/AllPermissions.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/AllPermissions.java new file mode 100644 index 00000000000..68eef0a4961 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/AllPermissions.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.spi.security.authorization; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; + +/** + * AllPermissions... TODO + */ +public final class AllPermissions implements CompiledPermissions { + + private static final CompiledPermissions INSTANCE = new AllPermissions(); + + private AllPermissions() {} + + public static CompiledPermissions getInstance() { + return INSTANCE; + } + + @Override + public boolean canRead(Tree tree) { + return true; + } + + @Override + public boolean canRead(Tree tree, PropertyState property) { + return true; + } + + @Override + public boolean isGranted(int permissions) { + return true; + } + + @Override + public boolean isGranted(Tree tree, int permissions) { + return true; + } + + @Override + public boolean isGranted(Tree parent, PropertyState property, int permissions) { + return true; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/CompiledPermissions.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/CompiledPermissions.java new file mode 100644 index 00000000000..bb4e9d03db2 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/CompiledPermissions.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.spi.security.authorization; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; + +/** + * CompiledPermissions... TODO + */ +public interface CompiledPermissions { + + boolean canRead(Tree tree); + + boolean canRead(Tree tree, PropertyState property); + + boolean isGranted(int permissions); + + boolean isGranted(Tree tree, int permissions); + + boolean isGranted(Tree parent, PropertyState property, int permissions); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/OpenAccessControlProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/OpenAccessControlProvider.java new file mode 100644 index 00000000000..6001a197ebd --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/OpenAccessControlProvider.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.authorization; + +import javax.security.auth.Subject; + +import org.apache.jackrabbit.oak.spi.security.SecurityConfiguration; + +/** + * This class implements an {@link AccessControlProvider} which grants + * full access to any {@link Subject} passed to {@link #getAccessControlContext(Subject)}. + */ +public class OpenAccessControlProvider extends SecurityConfiguration.Default + implements AccessControlProvider { + + @Override + public AccessControlContext getAccessControlContext(Subject subject) { + return new AccessControlContext() { + @Override + public CompiledPermissions getPermissions() { + return AllPermissions.getInstance(); + } + }; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/Permissions.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/Permissions.java new file mode 100644 index 00000000000..cb837fff3f6 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/Permissions.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.authorization; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Permissions... TODO + */ +public final class Permissions { + + public static final int NO_PERMISSION = 0; + + public static final int READ_NODE = 1; + + public static final int READ_PROPERTY = READ_NODE << 1; + + public static final int ADD_PROPERTY = READ_PROPERTY << 1; + + public static final int MODIFY_PROPERTY = ADD_PROPERTY << 1; + + public static final int REMOVE_PROPERTY = MODIFY_PROPERTY << 1; + + public static final int ADD_NODE = REMOVE_PROPERTY << 1; + + public static final int REMOVE_NODE = ADD_NODE << 1; + + public static final int READ_ACCESS_CONTROL = REMOVE_NODE << 1; + + public static final int MODIFY_ACCESS_CONTROL = READ_ACCESS_CONTROL << 1; + + public static final int NODE_TYPE_MANAGEMENT = MODIFY_ACCESS_CONTROL << 1; + + public static final int VERSION_MANAGEMENT = NODE_TYPE_MANAGEMENT << 1; + + public static final int LOCK_MANAGEMENT = VERSION_MANAGEMENT << 1; + + public static final int LIFECYCLE_MANAGEMENT = LOCK_MANAGEMENT << 1; + + public static final int RETENTION_MANAGEMENT = LIFECYCLE_MANAGEMENT << 1; + + public static final int MODIFY_CHILD_NODE_COLLECTION = RETENTION_MANAGEMENT << 1; + + public static final int NODE_TYPE_DEFINITION_MANAGEMENT = MODIFY_CHILD_NODE_COLLECTION << 1; + + public static final int NAMESPACE_MANAGEMENT = NODE_TYPE_DEFINITION_MANAGEMENT << 1; + + public static final int WORKSPACE_MANAGEMENT = NAMESPACE_MANAGEMENT << 1; + + public static final int PRIVILEGE_MANAGEMENT = WORKSPACE_MANAGEMENT << 1; + + public static final int USER_MANAGEMENT = PRIVILEGE_MANAGEMENT << 1; + + public static final int READ = READ_NODE | READ_PROPERTY; + + public static final int ALL = (READ + | ADD_PROPERTY | MODIFY_PROPERTY | REMOVE_PROPERTY + | ADD_NODE | REMOVE_NODE + | READ_ACCESS_CONTROL | MODIFY_ACCESS_CONTROL + | NODE_TYPE_MANAGEMENT + | VERSION_MANAGEMENT + | LOCK_MANAGEMENT + | LIFECYCLE_MANAGEMENT + | RETENTION_MANAGEMENT + | MODIFY_CHILD_NODE_COLLECTION + | NODE_TYPE_DEFINITION_MANAGEMENT + | NAMESPACE_MANAGEMENT + | WORKSPACE_MANAGEMENT + | PRIVILEGE_MANAGEMENT + | USER_MANAGEMENT + ); + + private static final Map PERMISSION_NAMES = new LinkedHashMap(); + static { + PERMISSION_NAMES.put(READ_NODE, "READ_NODE"); + PERMISSION_NAMES.put(READ_PROPERTY, "READ_PROPERTY"); + PERMISSION_NAMES.put(ADD_PROPERTY, "ADD_PROPERTY"); + PERMISSION_NAMES.put(MODIFY_PROPERTY, "MODIFY_PROPERTY"); + PERMISSION_NAMES.put(REMOVE_PROPERTY, "REMOVE_PROPERTY"); + PERMISSION_NAMES.put(ADD_NODE, "ADD_NODE"); + PERMISSION_NAMES.put(REMOVE_NODE, "REMOVE_NODE"); + PERMISSION_NAMES.put(MODIFY_CHILD_NODE_COLLECTION, "MODIFY_CHILD_NODE_COLLECTION"); + PERMISSION_NAMES.put(READ_ACCESS_CONTROL, "READ_ACCESS_CONTROL"); + PERMISSION_NAMES.put(MODIFY_ACCESS_CONTROL, "MODIFY_ACCESS_CONTROL"); + PERMISSION_NAMES.put(NODE_TYPE_MANAGEMENT, "NODE_TYPE_MANAGEMENT"); + PERMISSION_NAMES.put(VERSION_MANAGEMENT, "VERSION_MANAGEMENT"); + PERMISSION_NAMES.put(LOCK_MANAGEMENT, "LOCK_MANAGEMENT"); + PERMISSION_NAMES.put(LIFECYCLE_MANAGEMENT, "LIFECYCLE_MANAGEMENT"); + PERMISSION_NAMES.put(RETENTION_MANAGEMENT, "RETENTION_MANAGEMENT"); + PERMISSION_NAMES.put(NODE_TYPE_DEFINITION_MANAGEMENT, "NODE_TYPE_DEFINITION_MANAGEMENT"); + PERMISSION_NAMES.put(NAMESPACE_MANAGEMENT, "NAMESPACE_MANAGEMENT"); + PERMISSION_NAMES.put(WORKSPACE_MANAGEMENT, "WORKSPACE_MANAGEMENT"); + PERMISSION_NAMES.put(PRIVILEGE_MANAGEMENT, "PRIVILEGE_MANAGEMENT"); + PERMISSION_NAMES.put(USER_MANAGEMENT, "USER_MANAGEMENT"); + } + + public static String getString(int permissions) { + if (PERMISSION_NAMES.containsKey(permissions)) { + return PERMISSION_NAMES.get(permissions); + } else { + StringBuilder sb = new StringBuilder(); + sb.append('|'); + for (int key : PERMISSION_NAMES.keySet()) { + if ((permissions & key) == key) { + sb.append(PERMISSION_NAMES.get(key)).append('|'); + } + } + return sb.toString(); + } + } + + public static boolean isRepositoryPermission(int permission) { + return permission == NAMESPACE_MANAGEMENT || + permission == NODE_TYPE_DEFINITION_MANAGEMENT || + permission == PRIVILEGE_MANAGEMENT || + permission == WORKSPACE_MANAGEMENT; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/AdminPrincipal.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/AdminPrincipal.java new file mode 100644 index 00000000000..af456a16787 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/AdminPrincipal.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.spi.security.principal; + +import java.security.Principal; + +/** + * Principal used to mark an administrator. The aim of this principal + * is to simplify the check whether a given set of principals is supplied with + * special (admin) access permissions. It may be used as the single or as + * additional non-group principal. + */ +public interface AdminPrincipal extends Principal { + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/CompositePrincipalProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/CompositePrincipalProvider.java new file mode 100644 index 00000000000..97465e057b9 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/CompositePrincipalProvider.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.principal; + +import java.security.Principal; +import java.security.acl.Group; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import com.google.common.collect.Iterators; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * {@code PrincipalProvider} implementation that aggregates a list of principal + * providers into a single. + */ +public class CompositePrincipalProvider implements PrincipalProvider { + + private static final Logger log = LoggerFactory.getLogger(CompositePrincipalProvider.class); + + private final List providers; + + public CompositePrincipalProvider(List providers) { + this.providers = checkNotNull(providers); + } + + //--------------------------------------------------< PrincipalProvider >--- + @Override + public Principal getPrincipal(String principalName) { + Principal principal = null; + for (int i = 0; i < providers.size() && principal == null; i++) { + principal = providers.get(i).getPrincipal(principalName); + + } + return principal; + } + + @Override + public Set getGroupMembership(Principal principal) { + Set groups = new HashSet(); + for (PrincipalProvider provider : providers) { + groups.addAll(provider.getGroupMembership(principal)); + } + return groups; + } + + @Override + public Set getPrincipals(String userID) { + Set principals = new HashSet(); + for (PrincipalProvider provider : providers) { + principals.addAll(provider.getPrincipals(userID)); + } + return principals; + } + + @Override + public Iterator findPrincipals(String nameHint, int searchType) { + Iterator[] iterators = new Iterator[providers.size()]; + int i = 0; + for (PrincipalProvider provider : providers) { + iterators[i++] = provider.findPrincipals(nameHint, searchType); + } + return Iterators.concat(iterators); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/EveryonePrincipal.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/EveryonePrincipal.java new file mode 100644 index 00000000000..7ba4f8937b8 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/EveryonePrincipal.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.principal; + +import java.security.Principal; +import java.util.Enumeration; + +import org.apache.jackrabbit.api.security.principal.JackrabbitPrincipal; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Built-in principal group that has every other principal as member. + */ +public class EveryonePrincipal implements JackrabbitPrincipal, java.security.acl.Group { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(EveryonePrincipal.class); + + public static final String NAME = "everyone"; + + private static final EveryonePrincipal INSTANCE = new EveryonePrincipal(); + + private EveryonePrincipal() { } + + public static EveryonePrincipal getInstance() { + return INSTANCE; + } + + //----------------------------------------------------------< Principal >--- + @Override + public String getName() { + return NAME; + } + + //--------------------------------------------------------------< Group >--- + @Override + public boolean addMember(Principal user) { + return false; + } + + @Override + public boolean removeMember(Principal user) { + throw new UnsupportedOperationException("Cannot remove a member from the everyone group."); + } + + @Override + public boolean isMember(Principal member) { + return !member.equals(this); + } + + @Override + public Enumeration members() { + throw new UnsupportedOperationException("Not implemented."); + } + + //-------------------------------------------------------------< Object >--- + + @Override + public int hashCode() { + return NAME.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } else if (obj instanceof JackrabbitPrincipal) { + JackrabbitPrincipal other = (JackrabbitPrincipal) obj; + return NAME.equals(other.getName()); + } + return false; + } + + @Override + public String toString() { + return NAME + " principal"; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/PrincipalConfiguration.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/PrincipalConfiguration.java new file mode 100644 index 00000000000..db5b213d7c0 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/PrincipalConfiguration.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.spi.security.principal; + +import javax.annotation.Nonnull; +import javax.jcr.Session; + +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.spi.security.SecurityConfiguration; + +/** + * PrincipalConfig... TODO + */ +public interface PrincipalConfiguration extends SecurityConfiguration { + + @Nonnull + public PrincipalManager getPrincipalManager(Session session, Root root, NamePathMapper namePathMapper); + + @Nonnull + public PrincipalProvider getPrincipalProvider(Root root, NamePathMapper namePathMapper); +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/PrincipalIteratorAdapter.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/PrincipalIteratorAdapter.java new file mode 100644 index 00000000000..41642bedf52 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/PrincipalIteratorAdapter.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.principal; + +import java.security.Principal; +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; +import javax.jcr.RangeIterator; + +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; +import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter; +import org.apache.jackrabbit.commons.iterator.RangeIteratorDecorator; + +/** + * Principal specific {@code RangeIteratorAdapter} implementing the + * {@code PrincipalIterator} interface. + */ +public class PrincipalIteratorAdapter extends RangeIteratorDecorator implements PrincipalIterator { + + /** + * Static instance of an empty {@link PrincipalIterator}. + */ + @SuppressWarnings("unchecked") + public static final PrincipalIteratorAdapter EMPTY = + new PrincipalIteratorAdapter((Iterator) RangeIteratorAdapter.EMPTY); + + /** + * Creates an adapter for the given {@link javax.jcr.RangeIterator}. + * + * @param iterator iterator of {@link java.security.Principal}s + */ + public PrincipalIteratorAdapter(RangeIterator iterator) { + super(iterator); + } + + /** + * Creates an adapter for the given {@link java.util.Iterator} of principals. + * + * @param iterator iterator of {@link java.security.Principal}s + */ + public PrincipalIteratorAdapter(Iterator iterator) { + super(new RangeIteratorAdapter(iterator)); + } + + /** + * Creates an iterator for the given collection of {@code Principal}s. + * + * @param collection collection of {@link Principal} objects. + */ + public PrincipalIteratorAdapter(Collection collection) { + super(new RangeIteratorAdapter(collection)); + } + + //----------------------------------------< AccessControlPolicyIterator >--- + /** + * Returns the next policy. + * + * @return next policy. + * @throws java.util.NoSuchElementException if there is no next policy. + */ + @Override + public Principal nextPrincipal() throws NoSuchElementException { + return (Principal) next(); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/PrincipalProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/PrincipalProvider.java new file mode 100644 index 00000000000..f362889dbb4 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/PrincipalProvider.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.spi.security.principal; + +import java.security.Principal; +import java.security.acl.Group; +import java.util.Iterator; +import java.util.Set; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +/** + * PrincipalProvider... TODO + */ +public interface PrincipalProvider { + + /** + * Returns the principal with the specified name or {@code null} if the + * principal does not exist. + * + * @param principalName the name of the principal to retrieve + * @return return the requested principal or {@code null} + */ + @CheckForNull + Principal getPrincipal(String principalName); + + /** + * Returns an iterator over all group principals for which the given + * principal is either direct or indirect member of. Thus for any principal + * returned in the iterator {@link java.security.acl.Group#isMember(Principal)} + * must return {@code true}. + *

    + * Example:
    + * If Principal is member of Group A, and Group A is member of + * Group B, this method will return Group A and Group B. + * + * @param principal the principal to return it's membership from. + * @return an iterator returning all groups the given principal is member of. + * @see java.security.acl.Group#isMember(java.security.Principal) + */ + @Nonnull + Set getGroupMembership(Principal principal); + + /** + * Tries to resolve the specified {@code userID} to a valid principal and + * it's group membership. This method returns an empty set if the + * specified ID cannot be resolved. + * + * @param userID A userID. + * @return The set of principals associated with the specified {@code userID} + * or an empty set if it cannot be resolved. + */ + @Nonnull + Set getPrincipals(String userID); + + /** + * Find the principals that match the specified nameHint and search type. + * + * @param nameHint A name hint to use for non-exact matching. + * @param searchType Limit the search to certain types of principals. Valid + * values are any of + *

    • {@link org.apache.jackrabbit.api.security.principal.PrincipalManager#SEARCH_TYPE_ALL}
    + *
    • {@link org.apache.jackrabbit.api.security.principal.PrincipalManager#SEARCH_TYPE_NOT_GROUP}
    + *
    • {@link org.apache.jackrabbit.api.security.principal.PrincipalManager#SEARCH_TYPE_GROUP}
    + * @return An iterator of principals. + */ + @Nonnull + Iterator findPrincipals(String nameHint, int searchType); +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/SystemPrincipal.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/SystemPrincipal.java new file mode 100644 index 00000000000..8e6d478fe75 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/SystemPrincipal.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.spi.security.principal; + +import java.security.Principal; + +/** + * Principal to mark an system internal subject. + */ +public final class SystemPrincipal implements Principal { + + public static final SystemPrincipal INSTANCE = new SystemPrincipal(); + + private SystemPrincipal() { } + + //----------------------------------------------------------< Principal >--- + @Override + public String getName() { + return "system"; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/TreeBasedPrincipal.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/TreeBasedPrincipal.java new file mode 100644 index 00000000000..81a51885af9 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/TreeBasedPrincipal.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.principal; + +import java.security.Principal; + +import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal; +import org.apache.jackrabbit.api.security.principal.JackrabbitPrincipal; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.namepath.PathMapper; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.jackrabbit.oak.api.Type.STRING; + +/** + * TreeBasedPrincipal... + */ +public class TreeBasedPrincipal implements ItemBasedPrincipal { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(TreeBasedPrincipal.class); + + private final String principalName; + private final String path; + private final PathMapper pathMapper; + + public TreeBasedPrincipal(Tree tree, PathMapper pathMapper) { + PropertyState prop = tree.getProperty(UserConstants.REP_PRINCIPAL_NAME); + if (prop == null) { + throw new IllegalArgumentException("Tree doesn't have rep:principalName property"); + } + this.principalName = prop.getValue(STRING); + this.pathMapper = pathMapper; + this.path = tree.getPath(); + } + + public TreeBasedPrincipal(String principalName, Tree tree, PathMapper pathMapper) { + this(principalName, tree.getPath(), pathMapper); + } + + public TreeBasedPrincipal(String principalName, String oakPath, PathMapper pathMapper) { + this.principalName = principalName; + this.pathMapper = pathMapper; + this.path = oakPath; + } + + public String getOakPath() { + return path; + } + + //-------------------------------------------------< ItemBasedPrincipal >--- + @Override + public String getPath() { + return pathMapper.getJcrPath(path); + } + + //----------------------------------------------------------< Principal >--- + @Override + public String getName() { + return principalName; + } + + //-------------------------------------------------------------< Object >--- + /** + * Two principals are equal, if their names are. + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof JackrabbitPrincipal) { + return principalName.equals(((Principal) obj).getName()); + } + return false; + } + + /** + * @return the hash code of the principals name. + * @see Object#hashCode() + */ + @Override + public int hashCode() { + return principalName.hashCode(); + } + + /** + * @see Object#toString() + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getName()).append(':').append(principalName); + return sb.toString(); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/privilege/PrivilegeConfiguration.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/privilege/PrivilegeConfiguration.java new file mode 100644 index 00000000000..a84b84d29e9 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/privilege/PrivilegeConfiguration.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.spi.security.privilege; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.spi.security.SecurityConfiguration; + +/** + * PrivilegeConfiguration... TODO + */ +public interface PrivilegeConfiguration extends SecurityConfiguration { + + @Nonnull + PrivilegeProvider getPrivilegeProvider(ContentSession contentSession, Root root); + + @Nonnull + PrivilegeManager getPrivilegeManager(ContentSession contentSession, Root root, NamePathMapper namePathMapper); +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/privilege/PrivilegeConstants.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/privilege/PrivilegeConstants.java new file mode 100644 index 00000000000..c53c81f9b2d --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/privilege/PrivilegeConstants.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.privilege; + +import org.apache.jackrabbit.JcrConstants; + +/** + * PrivilegeConstants... TODO + */ +public interface PrivilegeConstants { + + // constants for privilege serialization + String REP_PRIVILEGES = "rep:privileges"; + String PRIVILEGES_PATH = '/' + JcrConstants.JCR_SYSTEM + '/' + REP_PRIVILEGES; + + String NT_REP_PRIVILEGES = "rep:Privileges"; + String NT_REP_PRIVILEGE = "rep:Privilege"; + + String REP_IS_ABSTRACT = "rep:isAbstract"; + String REP_AGGREGATES = "rep:aggregates"; + + // Constants for privilege names + String JCR_READ = "jcr:read"; + String JCR_MODIFY_PROPERTIES = "jcr:modifyProperties"; + String JCR_ADD_CHILD_NODES = "jcr:addChildNodes"; + String JCR_REMOVE_NODE = "jcr:removeNode"; + String JCR_REMOVE_CHILD_NODES = "jcr:removeChildNodes"; + String JCR_WRITE = "jcr:write"; + String JCR_READ_ACCESS_CONTROL = "jcr:readAccessControl"; + String JCR_MODIFY_ACCESS_CONTROL = "jcr:modifyAccessControl"; + String JCR_LOCK_MANAGEMENT = "jcr:lockManagement"; + String JCR_VERSION_MANAGEMENT = "jcr:versionManagement"; + String JCR_NODE_TYPE_MANAGEMENT = "jcr:nodeTypeManagement"; + String JCR_RETENTION_MANAGEMENT = "jcr:retentionManagement"; + String JCR_LIFECYCLE_MANAGEMENT = "jcr:lifecycleManagement"; + String JCR_WORKSPACE_MANAGEMENT = "jcr:workspaceManagement"; + String JCR_NODE_TYPE_DEFINITION_MANAGEMENT = "jcr:nodeTypeDefinitionManagement"; + String JCR_NAMESPACE_MANAGEMENT = "jcr:namespaceManagement"; + String JCR_ALL = "jcr:all"; + + String REP_PRIVILEGE_MANAGEMENT = "rep:privilegeManagement"; + String REP_WRITE = "rep:write"; + String REP_READ_NODES = "rep:readNodes"; + String REP_READ_PROPERTIES = "rep:readProperties"; + String REP_ADD_PROPERTIES = "rep:addProperties"; + String REP_ALTER_PROPERTIES = "rep:alterProperties"; + String REP_REMOVE_PROPERTIES = "rep:removeProperties"; + + String[] NON_AGGR_PRIVILEGES = new String[] { + REP_READ_NODES, REP_READ_PROPERTIES, + REP_ADD_PROPERTIES, REP_ALTER_PROPERTIES, REP_REMOVE_PROPERTIES, + JCR_ADD_CHILD_NODES, JCR_REMOVE_CHILD_NODES, JCR_REMOVE_NODE, + JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL, JCR_NODE_TYPE_MANAGEMENT, + JCR_VERSION_MANAGEMENT, JCR_LOCK_MANAGEMENT, JCR_LIFECYCLE_MANAGEMENT, + JCR_RETENTION_MANAGEMENT, JCR_WORKSPACE_MANAGEMENT, JCR_NODE_TYPE_DEFINITION_MANAGEMENT, + JCR_NAMESPACE_MANAGEMENT, REP_PRIVILEGE_MANAGEMENT}; + + String[] AGGR_PRIVILEGES = new String[] { + JCR_READ, JCR_MODIFY_PROPERTIES, JCR_WRITE, REP_WRITE + }; + + String[] AGGR_JCR_READ = new String[] { + REP_READ_NODES, REP_READ_PROPERTIES + }; + + String[] AGGR_JCR_MODIFY_PROPERTIES = new String[] { + REP_ADD_PROPERTIES, REP_ALTER_PROPERTIES, REP_REMOVE_PROPERTIES + }; + + String[] AGGR_JCR_WRITE = new String[] { + JCR_MODIFY_PROPERTIES, JCR_ADD_CHILD_NODES, JCR_REMOVE_CHILD_NODES, JCR_REMOVE_NODE + }; + + String[] AGGR_REP_WRITE = new String[] { + JCR_WRITE, JCR_NODE_TYPE_MANAGEMENT + }; +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/privilege/PrivilegeDefinition.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/privilege/PrivilegeDefinition.java new file mode 100644 index 00000000000..7d12d58398a --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/privilege/PrivilegeDefinition.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.spi.security.privilege; + +import java.util.Set; +import javax.annotation.Nonnull; + +/** + * The {@code PrivilegeDefinition} interface defines the characteristics of + * a JCR {@link javax.jcr.security.Privilege}. + */ +public interface PrivilegeDefinition { + + /** + * The internal name of this privilege. + * + * @return the internal name. + */ + @Nonnull + String getName(); + + /** + * Returns {@code true} if the privilege described by this definition + * is abstract. + * + * @return {@code true} if the resulting privilege is abstract; + * {@code false} otherwise. + */ + boolean isAbstract(); + + /** + * Returns the internal names of the declared aggregated privileges or + * an empty array if the privilege defined by this definition isn't + * an aggregate. + * + * @return The internal names of the aggregated privileges or an empty array. + */ + @Nonnull + Set getDeclaredAggregateNames(); +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/privilege/PrivilegeManagerImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/privilege/PrivilegeManagerImpl.java new file mode 100644 index 00000000000..99ebf327207 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/privilege/PrivilegeManagerImpl.java @@ -0,0 +1,199 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.privilege; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import javax.jcr.InvalidItemStateException; +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * PrivilegeManagerImpl... TODO + */ +public class PrivilegeManagerImpl implements PrivilegeManager { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(PrivilegeManagerImpl.class); + + private final Root root; + private final NamePathMapper namePathMapper; + + private final PrivilegeProvider provider; + + public PrivilegeManagerImpl(Root root, PrivilegeProvider provider, NamePathMapper namePathMapper) { + this.root = root; + this.namePathMapper = namePathMapper; + this.provider = provider; + } + + // TODO: review + public void refresh() { + provider.refresh(); + } + + @Override + public Privilege[] getRegisteredPrivileges() throws RepositoryException { + Set privileges = new HashSet(); + for (PrivilegeDefinition def : provider.getPrivilegeDefinitions()) { + privileges.add(new PrivilegeImpl(def)); + } + return privileges.toArray(new Privilege[privileges.size()]); + } + + @Override + public Privilege getPrivilege(String privilegeName) throws RepositoryException { + PrivilegeDefinition def = provider.getPrivilegeDefinition(getOakName(privilegeName)); + if (def == null) { + throw new AccessControlException("No such privilege " + privilegeName); + } else { + return new PrivilegeImpl(def); + } + } + + @Override + public Privilege registerPrivilege(String privilegeName, boolean isAbstract, + String[] declaredAggregateNames) throws RepositoryException { + if (root.hasPendingChanges()) { + throw new InvalidItemStateException("Session has pending changes."); + } + if (privilegeName == null || privilegeName.isEmpty()) { + throw new RepositoryException("Invalid privilege name " + privilegeName); + } + String oakName = getOakName(privilegeName); + if (oakName == null) { + throw new NamespaceException("Invalid privilege name " + privilegeName); + } + + PrivilegeDefinition def = provider.registerDefinition(oakName, isAbstract, getOakNames(declaredAggregateNames)); + return new PrivilegeImpl(def); + } + + //------------------------------------------------------------< private >--- + + private String getOakName(String jcrName) { + return namePathMapper.getOakName(jcrName); + } + + private Set getOakNames(String[] jcrNames) throws RepositoryException { + Set oakNames; + if (jcrNames == null || jcrNames.length == 0) { + oakNames = Collections.emptySet(); + } else { + oakNames = new HashSet(jcrNames.length); + for (String jcrName : jcrNames) { + String oakName = getOakName(jcrName); + if (oakName == null) { + throw new RepositoryException("Invalid name " + jcrName); + } + oakNames.add(oakName); + } + } + return oakNames; + } + + //-------------------------------------------------------------------------- + /** + * Privilege implementation based on a {@link PrivilegeDefinition}. + */ + private class PrivilegeImpl implements Privilege { + + private final PrivilegeDefinition definition; + + private PrivilegeImpl(PrivilegeDefinition definition) { + this.definition = definition; + } + + //------------------------------------------------------< Privilege >--- + @Override + public String getName() { + return getOakName(definition.getName()); + } + + @Override + public boolean isAbstract() { + return definition.isAbstract(); + } + + @Override + public boolean isAggregate() { + return !definition.getDeclaredAggregateNames().isEmpty(); + } + + @Override + public Privilege[] getDeclaredAggregatePrivileges() { + Set declaredAggregateNames = definition.getDeclaredAggregateNames(); + Set declaredAggregates = new HashSet(declaredAggregateNames.size()); + for (String pName : declaredAggregateNames) { + try { + declaredAggregates.add(getPrivilege(pName)); + } catch (RepositoryException e) { + log.warn("Error while retrieving privilege "+ pName +" contained in " + getName(), e.getMessage()); + } + } + return declaredAggregates.toArray(new Privilege[declaredAggregates.size()]); + } + + @Override + public Privilege[] getAggregatePrivileges() { + Set aggr = new HashSet(); + for (Privilege decl : getDeclaredAggregatePrivileges()) { + aggr.add(decl); + if (decl.isAggregate()) { + // TODO: defensive check to prevent circular aggregation that might occur with inconsistent repositories + aggr.addAll(Arrays.asList(decl.getAggregatePrivileges())); + } + } + return aggr.toArray(new Privilege[aggr.size()]); + } + + //---------------------------------------------------------< Object >--- + @Override + public int hashCode() { + return definition.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o instanceof PrivilegeImpl) { + return definition.equals(((PrivilegeImpl) o).definition); + } else { + return false; + } + } + + @Override + public String toString() { + return "Privilege " + definition.getName(); + } + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/privilege/PrivilegeProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/privilege/PrivilegeProvider.java new file mode 100644 index 00000000000..841058f1dc9 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/privilege/PrivilegeProvider.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.spi.security.privilege; + +import java.util.Set; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; + +/** + * PrivilegeProvider... TODO + */ +public interface PrivilegeProvider { + + /** + * Refresh this privilege provider. + */ + void refresh(); + + /** + * Returns all privilege definitions accessible to this provider. + * + * @return all privilege definitions. + */ + @Nonnull + PrivilegeDefinition[] getPrivilegeDefinitions(); + + /** + * Returns the privilege definition with the specified internal name. + * + * @param name The internal name of the privilege definition to be + * retrieved. + * @return The privilege definition with the given name or {@code null} if + * no such definition exists. + */ + @Nullable + PrivilegeDefinition getPrivilegeDefinition(String name); + + /** + * Creates and registers a new custom privilege definition with the specified + * characteristics. If the registration succeeds the new definition is + * returned; otherwise an {@code RepositoryException} is thrown. + * + * @param privilegeName The name of the definition. + * @param isAbstract {@code true} if the privilege is abstract. + * @param declaredAggregateNames The set of declared aggregate privilege names. + * @return The new definition. + * @throws RepositoryException If the definition could not be registered. + */ + PrivilegeDefinition registerDefinition(String privilegeName, boolean isAbstract, Set declaredAggregateNames) throws RepositoryException; +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/AuthorizableType.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/AuthorizableType.java new file mode 100644 index 00000000000..c6bde08f8b0 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/AuthorizableType.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.spi.security.user; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.UserManager; + +/** + * The different authorizable types. + */ +public enum AuthorizableType { + + USER(UserManager.SEARCH_TYPE_USER), + GROUP(UserManager.SEARCH_TYPE_GROUP), + AUTHORIZABLE(UserManager.SEARCH_TYPE_AUTHORIZABLE); + + private final int userType; + + private AuthorizableType(int jcrUserType) { + this.userType = jcrUserType; + } + + public static AuthorizableType getType(int jcrUserType) { + switch (jcrUserType) { + case UserManager.SEARCH_TYPE_AUTHORIZABLE: + return AUTHORIZABLE; + case UserManager.SEARCH_TYPE_GROUP: + return GROUP; + case UserManager.SEARCH_TYPE_USER: + return USER; + default: + throw new IllegalArgumentException("Invalid authorizable type "+jcrUserType); + } + } + + public boolean isType(Authorizable authorizable) { + if (authorizable == null) { + return false; + } + switch (userType) { + case UserManager.SEARCH_TYPE_GROUP: + return authorizable.isGroup(); + case UserManager.SEARCH_TYPE_USER: + return !authorizable.isGroup(); + default: + // TYPE_AUTHORIZABLE: + return true; + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConfiguration.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConfiguration.java new file mode 100644 index 00000000000..f828e0fdf04 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConfiguration.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.spi.security.user; + +import java.util.List; +import javax.annotation.Nonnull; +import javax.jcr.Session; + +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.spi.security.SecurityConfiguration; +import org.apache.jackrabbit.oak.spi.security.user.action.AuthorizableAction; + +/** + * UserContext... TODO + */ +public interface UserConfiguration extends SecurityConfiguration { + + @Nonnull + List getAuthorizableActions(); + + @Nonnull + UserManager getUserManager(Root root, NamePathMapper namePathMapper, Session session); + + @Nonnull + UserManager getUserManager(Root root, NamePathMapper namePathMapper); +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConstants.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConstants.java new file mode 100644 index 00000000000..728e6533c8c --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConstants.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.user; + +/** + * UserConstants... + */ +public interface UserConstants { + + String NT_REP_AUTHORIZABLE = "rep:Authorizable"; + String NT_REP_AUTHORIZABLE_FOLDER = "rep:AuthorizableFolder"; + String NT_REP_USER = "rep:User"; + String NT_REP_GROUP = "rep:Group"; + String NT_REP_MEMBERS = "rep:Members"; + String REP_PRINCIPAL_NAME = "rep:principalName"; + String REP_AUTHORIZABLE_ID = "rep:authorizableId"; + String REP_PASSWORD = "rep:password"; + String REP_DISABLED = "rep:disabled"; + String REP_MEMBERS = "rep:members"; + String REP_IMPERSONATORS = "rep:impersonators"; + + /** + * Configuration option defining the ID of the administrator user. + */ + String PARAM_ADMIN_ID = "adminId"; + + /** + * Default value for {@link #PARAM_ADMIN_ID} + */ + String DEFAULT_ADMIN_ID = "admin"; + + /** + * Configuration option defining the ID of the anonymous user. The ID + * might be {@code null} of no anonymous user exists. In this case + * Session#getUserID() may return {@code null} if it has been obtained + * using {@link javax.jcr.GuestCredentials}. + */ + String PARAM_ANONYMOUS_ID = "anonymousId"; + + /** + * Default value for {@link #PARAM_ANONYMOUS_ID} + */ + String DEFAULT_ANONYMOUS_ID = "anonymous"; + + /** + * Configuration option to define the path underneath which user nodes + * are being created. + */ + String PARAM_USER_PATH = "usersPath"; + + /** + * Default value for {@link #PARAM_USER_PATH} + */ + String DEFAULT_USER_PATH = "/rep:security/rep:authorizables/rep:users"; + + /** + * Configuration option to define the path underneath which group nodes + * are being created. + */ + String PARAM_GROUP_PATH = "groupsPath"; + + /** + * Default value for {@link #PARAM_GROUP_PATH} + */ + String DEFAULT_GROUP_PATH = "/rep:security/rep:authorizables/rep:groups"; + + /** + * Parameter used to change the number of levels that are used by default + * store authorizable nodes.
    The default number of levels is 2. + */ + String PARAM_DEFAULT_DEPTH = "defaultDepth"; + + /** + * Default value for {@link #PARAM_DEFAULT_DEPTH} + */ + int DEFAULT_DEPTH = 2; + + /** + * Its value determines the maximum number of members within a given + * content structure until additional intermediate structuring is being + * added. This may for example be used to + *
      + *
    • switch storing group members in JCR properties or nodes
    • + *
    • define maximum number of members is a multivalued property
    • + *
    • define maximum number of member properties within a given + * node structure
    • + *
    + */ + String PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE = "groupMembershipSplitSize"; + /** + * Configuration parameter to change the default algorithm used to generate + * password hashes. + */ + String PARAM_PASSWORD_HASH_ALGORITHM = "passwordHashAlgorithm"; + /** + * Configuration parameter to change the number of iterations used for + * password hash generation. + */ + String PARAM_PASSWORD_HASH_ITERATIONS = "passwordHashIterations"; + /** + * Configuration parameter to change the number of iterations used for + * password hash generation. + */ + String PARAM_PASSWORD_SALT_SIZE = "passwordSaltSize"; +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/AbstractAuthorizableAction.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/AbstractAuthorizableAction.java new file mode 100644 index 00000000000..f3b6f3de049 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/AbstractAuthorizableAction.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.user.action; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; + +/** + * Abstract implementation of the {@code AuthorizableAction} interface that + * doesn't perform any action. This is a convenience implementation allowing + * subclasses to only implement methods that need extra attention. + */ +public abstract class AbstractAuthorizableAction implements AuthorizableAction { + + /** + * Doesn't perform any action. + * + * @see AuthorizableAction#onCreate(org.apache.jackrabbit.api.security.user.Group, org.apache.jackrabbit.oak.api.Root, org.apache.jackrabbit.oak.namepath.NamePathMapper) + */ + @Override + public void onCreate(Group group, Root root, NamePathMapper namePathMapper) throws RepositoryException { + // nothing to do + } + + /** + * Doesn't perform any action. + * + * @see AuthorizableAction#onCreate(org.apache.jackrabbit.api.security.user.User, String, org.apache.jackrabbit.oak.api.Root, org.apache.jackrabbit.oak.namepath.NamePathMapper) + */ + @Override + public void onCreate(User user, String password, Root root, NamePathMapper namePathMapper) throws RepositoryException { + // nothing to do + } + + /** + * Doesn't perform any action. + * + * @see AuthorizableAction#onRemove(org.apache.jackrabbit.api.security.user.Authorizable, org.apache.jackrabbit.oak.api.Root, org.apache.jackrabbit.oak.namepath.NamePathMapper) + */ + @Override + public void onRemove(Authorizable authorizable, Root root, NamePathMapper namePathMapper) throws RepositoryException { + // nothing to do + } + + /** + * Doesn't perform any action. + * + * @see AuthorizableAction#onPasswordChange(org.apache.jackrabbit.api.security.user.User, String, org.apache.jackrabbit.oak.api.Root, org.apache.jackrabbit.oak.namepath.NamePathMapper) + */ + @Override + public void onPasswordChange(User user, String newPassword, Root root, NamePathMapper namePathMapper) throws RepositoryException { + // nothing to do + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/AccessControlAction.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/AccessControlAction.java new file mode 100644 index 00000000000..c9d2108a720 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/AccessControlAction.java @@ -0,0 +1,214 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.user.action; + +import java.util.ArrayList; +import java.util.List; +import javax.jcr.RepositoryException; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@code AccessControlAction} allows to setup permissions upon creation + * of a new authorizable; namely the privileges the new authorizable should be + * granted on it's own 'home directory' being represented by the new node + * associated with that new authorizable. + * + *

    The following to configuration parameters are available with this implementation: + *

      + *
    • groupPrivilegeNames: the value is expected to be a + * comma separated list of privileges that will be granted to the new group on + * the group node
    • + *
    • userPrivilegeNames: the value is expected to be a + * comma separated list of privileges that will be granted to the new user on + * the user node.
    • + *
    + *

    + *

    Example configuration: + *

    + *    groupPrivilegeNames : "jcr:read"
    + *    userPrivilegeNames  : "jcr:read, rep:write"
    + * 
    + *

    + *

    This configuration could for example lead to the following content + * structure upon user or group creation. Note however that the resulting + * structure depends on the actual access control management being in place: + * + *

    + *     UserManager umgr = ((JackrabbitSession) session).getUserManager();
    + *     User user = umgr.createUser("testUser", "t");
    + *
    + *     + t                           rep:AuthorizableFolder
    + *       + te                        rep:AuthorizableFolder
    + *         + testUser                rep:User, mix:AccessControllable
    + *           + rep:policy            rep:ACL
    + *             + allow               rep:GrantACE
    + *               - rep:principalName = "testUser"
    + *               - rep:privileges    = ["jcr:read","rep:write"]
    + *           - rep:password
    + *           - rep:principalName     = "testUser"
    + * 
    + * + *
    + *     UserManager umgr = ((JackrabbitSession) session).getUserManager();
    + *     Group group = umgr.createGroup("testGroup");
    + *
    + *     + t                           rep:AuthorizableFolder
    + *       + te                        rep:AuthorizableFolder
    + *         + testGroup               rep:Group, mix:AccessControllable
    + *           + rep:policy            rep:ACL
    + *             + allow               rep:GrantACE
    + *               - rep:principalName = "testGroup"
    + *               - rep:privileges    = ["jcr:read"]
    + *           - rep:principalName     = "testGroup"
    + * 
    + *

    + */ +public class AccessControlAction extends AbstractAuthorizableAction { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(AccessControlAction.class); + + private String[] groupPrivilegeNames = new String[0]; + private String[] userPrivilegeNames = new String[0]; + + //-------------------------------------------------< AuthorizableAction >--- + @Override + public void onCreate(Group group, Root root, NamePathMapper namePathMapper) throws RepositoryException { + setAC(group, root); + } + + @Override + public void onCreate(User user, String password, Root root, NamePathMapper namePathMapper) throws RepositoryException { + setAC(user, root); + } + + //------------------------------------------------------< Configuration >--- + /** + * Sets the privileges a new group will be granted on the group's home directory. + * + * @param privilegeNames A comma separated list of privilege names. + */ + public void setGroupPrivilegeNames(String privilegeNames) { + if (privilegeNames != null && privilegeNames.length() > 0) { + groupPrivilegeNames = split(privilegeNames); + } + + } + + /** + * Sets the privileges a new user will be granted on the user's home directory. + * + * @param privilegeNames A comma separated list of privilege names. + */ + public void setUserPrivilegeNames(String privilegeNames) { + if (privilegeNames != null && privilegeNames.length() > 0) { + userPrivilegeNames = split(privilegeNames); + } + } + + //------------------------------------------------------------< private >--- + + private void setAC(Authorizable authorizable, Root root) throws RepositoryException { + // TODO: add implementation + log.error("Not yet implemented"); + +// Node aNode; +// String path = authorizable.getPath(); +// +// JackrabbitAccessControlList acl = null; +// AccessControlManager acMgr = session.getAccessControlManager(); +// for (AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); it.hasNext();) { +// AccessControlPolicy plc = it.nextAccessControlPolicy(); +// if (plc instanceof JackrabbitAccessControlList) { +// acl = (JackrabbitAccessControlList) plc; +// break; +// } +// } +// +// if (acl == null) { +// log.warn("Cannot process AccessControlAction: no applicable ACL at " + path); +// } else { +// // setup acl according to configuration. +// Principal principal = authorizable.getPrincipal(); +// boolean modified = false; +// if (authorizable.isGroup()) { +// // new authorizable is a Group +// if (groupPrivilegeNames.length > 0) { +// modified = acl.addAccessControlEntry(principal, getPrivileges(groupPrivilegeNames, acMgr)); +// } +// } else { +// // new authorizable is a User +// if (userPrivilegeNames.length > 0) { +// modified = acl.addAccessControlEntry(principal, getPrivileges(userPrivilegeNames, acMgr)); +// } +// } +// if (modified) { +// acMgr.setPolicy(path, acl); +// } +// } + } + + /** + * Retrieve privileges for the specified privilege names. + * + * @param privNames The privilege names. + * @param acMgr The access control manager. + * @return Array of {@code Privilege} + * @throws javax.jcr.RepositoryException If a privilege name cannot be + * resolved to a valid privilege. + */ + private static Privilege[] getPrivileges(String[] privNames, AccessControlManager acMgr) throws RepositoryException { + if (privNames == null || privNames.length == 0) { + return new Privilege[0]; + } + Privilege[] privileges = new Privilege[privNames.length]; + for (int i = 0; i < privNames.length; i++) { + privileges[i] = acMgr.privilegeFromName(privNames[i]); + } + return privileges; + } + + /** + * Split the specified configuration parameter into privilege names. + * + * @param configParam The configuration parameter defining a comma separated + * list of privilege names. + * @return An array of privilege names. + */ + private static String[] split(String configParam) { + List nameList = new ArrayList(); + for (String pn : Text.explode(configParam, ',', false)) { + String privName = pn.trim(); + if (privName.length() > 0) { + nameList.add(privName); + } + } + return nameList.toArray(new String[nameList.size()]); + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/AuthorizableAction.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/AuthorizableAction.java new file mode 100644 index 00000000000..ff1bf9a8b94 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/AuthorizableAction.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.user.action; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; + +/** + * The {@code AuthorizableAction} interface provide an implementation + * specific way to execute additional validation or write tasks upon + * + *
      + *
    • {@link #onCreate User creation},
    • + *
    • {@link #onCreate Group creation},
    • + *
    • {@link #onRemove Authorizable removal} and
    • + *
    • {@link #onPasswordChange User password modification}.
    • + *
    + * + * Note, that in contrast to {@link org.apache.jackrabbit.oak.spi.commit.Validator} + * the authorizable actions will only be enforced when user related content + * modifications are generated by using the user management API. + * + * @see org.apache.jackrabbit.oak.spi.security.ConfigurationParameters + */ +public interface AuthorizableAction { + + /** + * Allows to add application specific modifications or validation associated + * with the creation of a new group. Note, that this method is called + * before any {@code Root#commit()} call. + * + * + * @param group The new group that has not yet been persisted; + * e.g. the associated tree is still 'NEW'. + * @param root The root associated with the user manager. + * @param namePathMapper + * @throws javax.jcr.RepositoryException If an error occurs. + */ + void onCreate(Group group, Root root, NamePathMapper namePathMapper) throws RepositoryException; + + /** + * Allows to add application specific modifications or validation associated + * with the creation of a new user. Note, that this method is called + * before any {@code Root#commit()} call. + * + * + * @param user The new user that has not yet been persisted; + * e.g. the associated tree is still 'NEW'. + * @param password The password that was specified upon user creation. + * @param root The root associated with the user manager. + * @param namePathMapper + * @throws RepositoryException If an error occurs. + */ + void onCreate(User user, String password, Root root, NamePathMapper namePathMapper) throws RepositoryException; + + /** + * Allows to add application specific behavior associated with the removal + * of an authorizable. Note, that this method is called before + * {@link org.apache.jackrabbit.api.security.user.Authorizable#remove} is executed (and persisted); thus the + * target authorizable still exists. + * + * + * @param authorizable The authorizable to be removed. + * @param root The root associated with the user manager. + * @param namePathMapper + * @throws RepositoryException If an error occurs. + */ + void onRemove(Authorizable authorizable, Root root, NamePathMapper namePathMapper) throws RepositoryException; + + /** + * Allows to add application specific action or validation associated with + * changing a user password. Note, that this method is called before + * the password property is being modified in the content. + * + * + * @param user The user that whose password is going to change. + * @param newPassword The new password as specified in {@link org.apache.jackrabbit.api.security.user.User#changePassword} + * @param root The root associated with the user manager. + * @param namePathMapper + * @throws RepositoryException If an exception or error occurs. + */ + void onPasswordChange(User user, String newPassword, Root root, NamePathMapper namePathMapper) throws RepositoryException; +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/ClearMembershipAction.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/ClearMembershipAction.java new file mode 100644 index 00000000000..64f7e14b003 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/ClearMembershipAction.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.user.action; + +import java.util.Iterator; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; + +/** + * Authorizable action attempting to clear all group membership before removing + * the specified authorizable. If {@link Group#removeMember(Authorizable)} + * fails due to lack of permissions {@link AuthorizableAction#onRemove(org.apache.jackrabbit.api.security.user.Authorizable, org.apache.jackrabbit.oak.api.Root, org.apache.jackrabbit.oak.namepath.NamePathMapper)} + * throws an exception and removing the specified authorizable will be aborted. + */ +public class ClearMembershipAction extends AbstractAuthorizableAction { + + //-------------------------------------------------< AuthorizableAction >--- + @Override + public void onRemove(Authorizable authorizable, Root root, NamePathMapper namePathMapper) throws RepositoryException { + clearMembership(authorizable); + } + + //-------------------------------------------------------------------------- + private static void clearMembership(Authorizable authorizable) throws RepositoryException { + Iterator membership = authorizable.declaredMemberOf(); + while (membership.hasNext()) { + membership.next().removeMember(authorizable); + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/PasswordValidationAction.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/PasswordValidationAction.java new file mode 100644 index 00000000000..d0e5e6d4bd3 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/PasswordValidationAction.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.user.action; + +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.spi.security.user.util.PasswordUtility; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@code PasswordValidationAction} provides a simple password validation + * mechanism with the following configurable option: + * + *
      + *
    • constraint: a regular expression that can be compiled + * to a {@link java.util.regex.Pattern} defining validation rules for a password.
    • + *
    + * + *

    The password validation is executed on user creation and upon password + * change. It throws a {@code ConstraintViolationException} if the password + * validation fails.

    + * + * @see org.apache.jackrabbit.api.security.user.UserManager#createUser(String, String) + * @see org.apache.jackrabbit.api.security.user.User#changePassword(String) + * @see org.apache.jackrabbit.api.security.user.User#changePassword(String, String) + */ +public class PasswordValidationAction extends AbstractAuthorizableAction { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(PasswordValidationAction.class); + + private Pattern pattern; + + //-------------------------------------------------< AuthorizableAction >--- + @Override + public void onCreate(User user, String password, Root root, NamePathMapper namePathMapper) throws RepositoryException { + validatePassword(password, false); + } + + @Override + public void onPasswordChange(User user, String newPassword, Root root, NamePathMapper namePathMapper) throws RepositoryException { + validatePassword(newPassword, true); + } + + //------------------------------------------------------< Configuration >--- + /** + * Set the password constraint. + * + * @param constraint A regular expression that can be used to validate a new password. + */ + public void setConstraint(String constraint) { + try { + pattern = Pattern.compile(constraint); + } catch (PatternSyntaxException e) { + log.warn("Invalid password constraint: ", e.getMessage()); + } + } + + //------------------------------------------------------------< private >--- + /** + * Validate the specified password. + * + * @param password The password to be validated + * @param forceMatch If true the specified password is always validated; + * otherwise only if it is a plain text password. + * @throws RepositoryException If the specified password is too short or + * doesn't match the specified password pattern. + */ + private void validatePassword(String password, boolean forceMatch) throws RepositoryException { + if (password != null && (forceMatch || PasswordUtility.isPlainTextPassword(password))) { + if (pattern != null && !pattern.matcher(password).matches()) { + throw new ConstraintViolationException("Password violates password constraint (" + pattern.pattern() + ")."); + } + } + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/util/PasswordUtility.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/util/PasswordUtility.java new file mode 100644 index 00000000000..386c4a42784 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/util/PasswordUtility.java @@ -0,0 +1,275 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.user.util; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import javax.annotation.Nullable; + +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utility to generate and compare password hashes. + */ +public class PasswordUtility { + + private static final Logger log = LoggerFactory.getLogger(PasswordUtility.class); + + private static final char DELIMITER = '-'; + private static final int NO_ITERATIONS = 1; + private static final String ENCODING = "UTF-8"; + + public static final String DEFAULT_ALGORITHM = "SHA-256"; + public static final int DEFAULT_SALT_SIZE = 8; + public static final int DEFAULT_ITERATIONS = 1000; + + /** + * Avoid instantiation + */ + private PasswordUtility() {} + + /** + * Generates a hash of the specified password with the default values + * for algorithm, salt-size and number of iterations. + * + * @param password The password to be hashed. + * @return The password hash. + * @throws NoSuchAlgorithmException If {@link #DEFAULT_ALGORITHM} is not supported. + * @throws UnsupportedEncodingException If utf-8 is not supported. + */ + public static String buildPasswordHash(String password) throws NoSuchAlgorithmException, UnsupportedEncodingException { + return buildPasswordHash(password, DEFAULT_ALGORITHM, DEFAULT_SALT_SIZE, DEFAULT_ITERATIONS); + } + + /** + * Generates a hash of the specified password using the specified algorithm, + * salt size and number of iterations into account. + * + * @param password The password to be hashed. + * @param algorithm The desired hash algorithm. + * @param saltSize The desired salt size. If the specified integer is lower + * that {@link #DEFAULT_SALT_SIZE} the default is used. + * @param iterations The desired number of iterations. If the specified + * integer is lower than 1 the {@link #DEFAULT_ITERATIONS default} value is used. + * @return The password hash. + * @throws NoSuchAlgorithmException If the specified algorithm is not supported. + * @throws UnsupportedEncodingException If utf-8 is not supported. + */ + public static String buildPasswordHash(String password, String algorithm, + int saltSize, int iterations) throws NoSuchAlgorithmException, UnsupportedEncodingException { + if (password == null) { + throw new IllegalArgumentException("Password may not be null."); + } + if (iterations < NO_ITERATIONS) { + iterations = DEFAULT_ITERATIONS; + } + if (saltSize < DEFAULT_SALT_SIZE) { + saltSize = DEFAULT_SALT_SIZE; + } + String salt = generateSalt(saltSize); + String alg = (algorithm == null) ? DEFAULT_ALGORITHM : algorithm; + return generateHash(password, alg, salt, iterations); + } + + /** + * Same as {@link #buildPasswordHash(String, String, int, int)} but retrieving + * the parameters for hash generation from the specified configuration. + * + * @param password The password to be hashed. + * @param config The configuration defining the details of the hash generation. + * @return The password hash. + * @throws NoSuchAlgorithmException If the specified algorithm is not supported. + * @throws UnsupportedEncodingException If utf-8 is not supported. + */ + public static String buildPasswordHash(String password, ConfigurationParameters config) throws NoSuchAlgorithmException, UnsupportedEncodingException { + if (config == null) { + throw new IllegalArgumentException("UserConfig must not be null"); + } + String algorithm = config.getConfigValue(UserConstants.PARAM_PASSWORD_HASH_ALGORITHM, DEFAULT_ALGORITHM); + int iterations = config.getConfigValue(UserConstants.PARAM_PASSWORD_HASH_ITERATIONS, DEFAULT_ITERATIONS); + int saltSize = config.getConfigValue(UserConstants.PARAM_PASSWORD_SALT_SIZE, DEFAULT_SALT_SIZE); + + return buildPasswordHash(password, algorithm, saltSize, iterations); + } + + /** + * Returns {@code true} if the specified string doesn't start with a + * valid algorithm name in curly brackets. + * + * @param password The string to be tested. + * @return {@code true} if the specified string doesn't start with a + * valid algorithm name in curly brackets. + */ + public static boolean isPlainTextPassword(@Nullable String password) { + return extractAlgorithm(password) == null; + } + + /** + * Returns {@code true} if hash of the specified {@code password} equals the + * given hashed password. + * + * @param hashedPassword Password hash. + * @param password The password to compare. + * @return If the hash created from the specified {@code password} equals + * the given {@code hashedPassword} string. + */ + public static boolean isSame(String hashedPassword, char[] password) { + return isSame(hashedPassword, String.valueOf(password)); + } + + /** + * Returns {@code true} if hash of the specified {@code password} equals the + * given hashed password. + * + * @param hashedPassword Password hash. + * @param password The password to compare. + * @return If the hash created from the specified {@code password} equals + * the given {@code hashedPassword} string. + */ + public static boolean isSame(String hashedPassword, String password) { + try { + String algorithm = extractAlgorithm(hashedPassword); + if (algorithm != null) { + int startPos = algorithm.length()+2; + String salt = extractSalt(hashedPassword, startPos); + int iterations = NO_ITERATIONS; + if (salt != null) { + startPos += salt.length()+1; + iterations = extractIterations(hashedPassword, startPos); + } + + String hash = generateHash(password, algorithm, salt, iterations); + return hashedPassword.equals(hash); + } // hashedPassword is plaintext -> return false + } catch (NoSuchAlgorithmException e) { + log.warn(e.getMessage()); + } catch (UnsupportedEncodingException e) { + log.warn(e.getMessage()); + } + return false; + } + + //------------------------------------------------------------< private >--- + + private static String generateHash(String pwd, String algorithm, String salt, int iterations) throws NoSuchAlgorithmException, UnsupportedEncodingException { + StringBuilder passwordHash = new StringBuilder(); + passwordHash.append('{').append(algorithm).append('}'); + if (salt != null && !salt.isEmpty()) { + StringBuilder data = new StringBuilder(); + data.append(salt).append(pwd); + + passwordHash.append(salt).append(DELIMITER); + if (iterations > NO_ITERATIONS) { + passwordHash.append(iterations).append(DELIMITER); + } + passwordHash.append(generateDigest(data.toString(), algorithm, iterations)); + } else { + // backwards compatible to jr 2.0: no salt, no iterations + passwordHash.append(Text.digest(algorithm, pwd.getBytes(ENCODING))); + } + return passwordHash.toString(); + } + + private static String generateSalt(int saltSize) { + SecureRandom random = new SecureRandom(); + byte[] salt = new byte[saltSize]; + random.nextBytes(salt); + + StringBuilder res = new StringBuilder(salt.length * 2); + for (byte b : salt) { + res.append(Text.hexTable[(b >> 4) & 15]); + res.append(Text.hexTable[b & 15]); + } + return res.toString(); + } + + private static String generateDigest(String data, String algorithm, int iterations) throws UnsupportedEncodingException, NoSuchAlgorithmException { + byte[] bytes = data.getBytes(ENCODING); + MessageDigest md = MessageDigest.getInstance(algorithm); + + for (int i = 0; i < iterations; i++) { + md.reset(); + bytes = md.digest(bytes); + } + + StringBuilder res = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + res.append(Text.hexTable[(b >> 4) & 15]); + res.append(Text.hexTable[b & 15]); + } + return res.toString(); + } + + /** + * Extract the algorithm from the given crypted password string. Returns the + * algorithm or {@code null} if the given string doesn't have a + * leading {@code algorithm} such as created by {@code buildPasswordHash} + * or if the extracted string doesn't represent an available algorithm. + * + * @param hashedPwd The password hash. + * @return The algorithm or {@code null} if the given string doesn't have a + * leading {@code algorithm} such as created by {@code buildPasswordHash} + * or if the extracted string isn't a supported algorithm. + */ + private static String extractAlgorithm(String hashedPwd) { + if (hashedPwd != null && !hashedPwd.isEmpty()) { + int end = hashedPwd.indexOf('}'); + if (hashedPwd.charAt(0) == '{' && end > 0 && end < hashedPwd.length()-1) { + String algorithm = hashedPwd.substring(1, end); + try { + MessageDigest.getInstance(algorithm); + return algorithm; + } catch (NoSuchAlgorithmException e) { + log.debug("Invalid algorithm detected " + algorithm); + } + } + } + + // not starting with {} or invalid algorithm + return null; + } + + private static String extractSalt(String hashedPwd, int start) { + int end = hashedPwd.indexOf(DELIMITER, start); + if (end > -1) { + return hashedPwd.substring(start, end); + } + // no salt + return null; + } + + private static int extractIterations(String hashedPwd, int start) { + int end = hashedPwd.indexOf(DELIMITER, start); + if (end > -1) { + String str = hashedPwd.substring(start, end); + try { + return Integer.parseInt(str); + } catch (NumberFormatException e) { + log.debug("Expected number of iterations. Found: " + str); + } + } + + // no extra iterations + return NO_ITERATIONS; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/util/UserUtility.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/util/UserUtility.java new file mode 100644 index 00000000000..b2635c56892 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/util/UserUtility.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.spi.security.user.util; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; + +import static org.apache.jackrabbit.oak.api.Type.STRING; + +/** + * UserUtils... TODO + */ +public final class UserUtility implements UserConstants{ + + @Nonnull + public static String getAdminId(ConfigurationParameters parameters) { + return parameters.getConfigValue(PARAM_ADMIN_ID, DEFAULT_ADMIN_ID); + } + + @Nonnull + public static String getAnonymousId(ConfigurationParameters parameters) { + return parameters.getConfigValue(PARAM_ANONYMOUS_ID, DEFAULT_ANONYMOUS_ID); + } + + public static boolean isType(Tree authorizableTree, AuthorizableType type) { + // FIXME: check for node type according to the specified type constraint + if (authorizableTree != null && authorizableTree.hasProperty(JcrConstants.JCR_PRIMARYTYPE)) { + String ntName = authorizableTree.getProperty(JcrConstants.JCR_PRIMARYTYPE).getValue(STRING); + switch (type) { + case GROUP: + return NT_REP_GROUP.equals(ntName); + case USER: + return NT_REP_USER.equals(ntName); + default: + return NT_REP_USER.equals(ntName) || NT_REP_GROUP.equals(ntName); + } + } + return false; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/model/AbstractChildNodeEntry.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/AbstractChildNodeEntry.java similarity index 84% rename from oak-core/src/main/java/org/apache/jackrabbit/oak/model/AbstractChildNodeEntry.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/AbstractChildNodeEntry.java index d9f7bf4bc96..8c3ff598db9 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/model/AbstractChildNodeEntry.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/AbstractChildNodeEntry.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.oak.model; +package org.apache.jackrabbit.oak.spi.state; /** * Abstract base class for {@link ChildNodeEntry} implementations. @@ -24,6 +24,16 @@ */ public abstract class AbstractChildNodeEntry implements ChildNodeEntry { + /** + * Returns a string representation of this child node entry. + * + * @return string representation + */ + @Override + public String toString() { + return getName(); + } + /** * Checks whether the given object is equal to this one. Two child node * entries are considered equal if both their names and referenced node @@ -31,8 +41,8 @@ public abstract class AbstractChildNodeEntry implements ChildNodeEntry { * equality check if one is available. * * @param that target of the comparison - * @return true if the objects are equal, - * false otherwise + * @return {@code true} if the objects are equal, + * {@code false} otherwise */ @Override public boolean equals(Object that) { @@ -41,7 +51,7 @@ public boolean equals(Object that) { } else if (that instanceof ChildNodeEntry) { ChildNodeEntry other = (ChildNodeEntry) that; return getName().equals(other.getName()) - && getNode().equals(other.getNode()); + && getNodeState().equals(other.getNodeState()); } else { return false; } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/AbstractNodeState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/AbstractNodeState.java new file mode 100644 index 00000000000..133fb2ea625 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/AbstractNodeState.java @@ -0,0 +1,254 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.state; + +import org.apache.jackrabbit.oak.api.PropertyState; + +import com.google.common.base.Function; +import com.google.common.collect.Iterables; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.annotation.Nonnull; + +/** + * Abstract base class for {@link NodeState} implementations. + * This base class contains default implementations of the + * {@link #equals(Object)} and {@link #hashCode()} methods based on + * the implemented interface. + *

    + * This class also implements trivial (and potentially very slow) versions of + * the {@link #getProperty(String)} and {@link #getPropertyCount()} methods + * based on {@link #getProperties()}. The {@link #getChildNode(String)} and + * {@link #getChildNodeCount()} methods are similarly implemented based on + * {@link #getChildNodeEntries()}. Subclasses should normally + * override these method with a more efficient alternatives. + */ +public abstract class AbstractNodeState implements NodeState { + + @Override + public PropertyState getProperty(String name) { + for (PropertyState property : getProperties()) { + if (name.equals(property.getName())) { + return property; + } + } + return null; + } + + @Override + public long getPropertyCount() { + return count(getProperties()); + } + + @Override + public boolean hasChildNode(String name) { + return getChildNode(name) != null; + } + + @Override + public NodeState getChildNode(String name) { + for (ChildNodeEntry entry : getChildNodeEntries()) { + if (name.equals(entry.getName())) { + return entry.getNodeState(); + } + } + return null; + } + + @Override + public long getChildNodeCount() { + return count(getChildNodeEntries()); + } + + @Override + public Iterable getChildNodeNames() { + return Iterables.transform( + getChildNodeEntries(), + new Function() { + @Override + public String apply(ChildNodeEntry input) { + return input.getName(); + } + }); + } + + @Override + public Iterable getChildNodeEntries() { + return Iterables.transform( + getChildNodeNames(), + new Function() { + @Override + public ChildNodeEntry apply(final String input) { + return new AbstractChildNodeEntry() { + @Override @Nonnull + public String getName() { + return input; + } + @Override @Nonnull + public NodeState getNodeState() { + return getChildNode(input); + } + }; + } + }); + } + + /** + * Generic default comparison algorithm that simply walks through the + * property and child node lists of the given base state and compares + * the entries one by one with corresponding ones (if any) in this state. + */ + @Override + public void compareAgainstBaseState(NodeState base, NodeStateDiff diff) { + Set baseProperties = new HashSet(); + for (PropertyState beforeProperty : base.getProperties()) { + String name = beforeProperty.getName(); + PropertyState afterProperty = getProperty(name); + if (afterProperty == null) { + diff.propertyDeleted(beforeProperty); + } else { + baseProperties.add(name); + if (!beforeProperty.equals(afterProperty)) { + diff.propertyChanged(beforeProperty, afterProperty); + } + } + } + for (PropertyState afterProperty : getProperties()) { + if (!baseProperties.contains(afterProperty.getName())) { + diff.propertyAdded(afterProperty); + } + } + + Set baseChildNodes = new HashSet(); + for (ChildNodeEntry beforeCNE : base.getChildNodeEntries()) { + String name = beforeCNE.getName(); + NodeState beforeChild = beforeCNE.getNodeState(); + NodeState afterChild = getChildNode(name); + if (afterChild == null) { + diff.childNodeDeleted(name, beforeChild); + } else { + baseChildNodes.add(name); + if (!beforeChild.equals(afterChild)) { + diff.childNodeChanged(name, beforeChild, afterChild); + } + } + } + for (ChildNodeEntry afterChild : getChildNodeEntries()) { + String name = afterChild.getName(); + if (!baseChildNodes.contains(name)) { + diff.childNodeAdded(name, afterChild.getNodeState()); + } + } + } + + /** + * Returns a string representation of this child node entry. + * + * @return string representation + */ + public String toString() { + StringBuilder builder = new StringBuilder("{"); + AtomicBoolean first = new AtomicBoolean(true); + for (PropertyState property : getProperties()) { + if (!first.getAndSet(false)) { + builder.append(','); + } + builder.append(' ').append(property); + } + for (ChildNodeEntry entry : getChildNodeEntries()) { + if (!first.getAndSet(false)) { + builder.append(','); + } + builder.append(' ').append(entry); + } + builder.append(" }"); + return builder.toString(); + } + + /** + * Checks whether the given object is equal to this one. Two node states + * are considered equal if all their properties and child nodes match, + * regardless of ordering. Subclasses may override this method with a + * more efficient equality check if one is available. + * + * @param that target of the comparison + * @return {@code true} if the objects are equal, + * {@code false} otherwise + */ + @Override + public boolean equals(Object that) { + if (this == that) { + return true; + } else if (that == null || !(that instanceof NodeState)) { + return false; + } + + NodeState other = (NodeState) that; + + if (getPropertyCount() != other.getPropertyCount() + || getChildNodeCount() != other.getChildNodeCount()) { + return false; + } + + for (PropertyState property : getProperties()) { + if (!property.equals(other.getProperty(property.getName()))) { + return false; + } + } + + // TODO inefficient unless there are very few child nodes + for (ChildNodeEntry entry : getChildNodeEntries()) { + if (!entry.getNodeState().equals( + other.getChildNode(entry.getName()))) { + return false; + } + } + + return true; + + } + + /** + * Returns a hash code that's compatible with how the + * {@link #equals(Object)} method is implemented. The current + * implementation simply returns zero for everything since + * {@link NodeState} instances are not intended for use as hash keys. + * + * @return hash code + */ + @Override + public int hashCode() { + return 0; + } + + //-----------------------------------------------------------< private >-- + + private static long count(Iterable iterable) { + long n = 0; + Iterator iterator = iterable.iterator(); + while (iterator.hasNext()) { + iterator.next(); + n++; + } + return n; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/model/ChildNodeEntry.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/ChildNodeEntry.java similarity index 77% rename from oak-core/src/main/java/org/apache/jackrabbit/oak/model/ChildNodeEntry.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/ChildNodeEntry.java index 96ca344e1d6..e74ac2d2e98 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/model/ChildNodeEntry.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/ChildNodeEntry.java @@ -14,11 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.oak.model; +package org.apache.jackrabbit.oak.spi.state; + + +import javax.annotation.Nonnull; /** - * TODO: document - * + * A {@code ChildNodeEntry} instance represents the child node states of a + * {@link NodeState}. *

    Equality and hash codes

    *

    * Two child node entries are considered equal if and only if their names @@ -31,13 +34,17 @@ public interface ChildNodeEntry { /** - * TODO: document + * The name of the child node state wrt. to its parent state. + * @return name of the child node */ + @Nonnull String getName(); /** - * TODO: document + * The child node state + * @return child node state */ - NodeState getNode(); + @Nonnull + NodeState getNodeState(); } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/DefaultNodeStateDiff.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/DefaultNodeStateDiff.java new file mode 100644 index 00000000000..da12ad62143 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/DefaultNodeStateDiff.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.spi.state; + +import org.apache.jackrabbit.oak.api.PropertyState; + +/** + * Node state diff handler that by default does nothing. Useful as a base + * class for more complicated diff handlers that can safely ignore one or + * more types of changes. + */ +public class DefaultNodeStateDiff implements NodeStateDiff { + + public static final NodeStateDiff INSTANCE = new DefaultNodeStateDiff(); + + @Override + public void propertyAdded(PropertyState after) { + // do nothing + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) { + // do nothing + } + + @Override + public void propertyDeleted(PropertyState before) { + // do nothing + } + + @Override + public void childNodeAdded(String name, NodeState after) { + // do nothing + } + + @Override + public void childNodeChanged(String name, NodeState before, NodeState after) { + // do nothing + } + + @Override + public void childNodeDeleted(String name, NodeState before) { + // do nothing + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/NodeBuilder.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/NodeBuilder.java new file mode 100644 index 00000000000..053ffb06ae1 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/NodeBuilder.java @@ -0,0 +1,192 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.state; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; + +/** + * Builder interface for constructing new {@link NodeState node states}. + */ +public interface NodeBuilder { + + /** + * Returns an immutable node state that matches the current state of + * the builder. + * + * @return immutable node state + */ + @Nonnull + NodeState getNodeState(); + + /** + * Returns the original base state that this builder is modifying. + * Returns {@code null} if this builder represents a new node that + * didn't exist in the base content tree. + * + * @return base node state, or {@code null} + */ + @CheckForNull + NodeState getBaseState(); + + /** + * Replaces the base state of this builder and throws away all changes. + * The effect of this method is equivalent to replacing this builder + * (and the connected subtree) with a new builder returned by + * {@code state.builder()}. + *

    + * This method only works on builders acquired directly from a call + * to {@link NodeState#builder()}. Calling it on a builder returned + * by the {@link #child(String)} method will throw an + * {@link IllegalStateException}. + * + * @param state new base state + * @throws IllegalStateException if this is not a root builder + */ + void reset(@Nonnull NodeState state) + throws IllegalStateException; + + /** + * Returns the current number of child nodes. + * + * @return number of child nodes + */ + long getChildNodeCount(); + + /** + * Checks whether the named child node currently exists. + * + * @param name child node name + * @return {@code true} if the named child node exists, + * {@code false} otherwise + */ + boolean hasChildNode(String name); + + /** + * Returns the names of current child nodes. + * + * @return child node names + */ + Iterable getChildNodeNames(); + + /** + * Adds or replaces a subtree. + * + * @param name name of the child node containing the new subtree + * @param nodeState subtree + * @return this builder + */ + @Nonnull + NodeBuilder setNode(String name, @Nonnull NodeState nodeState); + + /** + * Remove a child node. This method has no effect if a + * name of the given {@code name} does not exist. + * + * @param name name of the child node + * @return this builder + */ + @Nonnull + NodeBuilder removeNode(String name); + + /** + * Returns the current number of properties. + * + * @return number of properties + */ + long getPropertyCount(); + + /** + * Returns the current properties. + * + * @return current properties + */ + Iterable getProperties(); + + /** + * Returns the current state of the named property, or {@code null} + * if the property is not set. + * + * @param name property name + * @return property state + */ + PropertyState getProperty(String name); + + /** + * Set a property state + * @param property The property state to set + * @return this builder + */ + NodeBuilder setProperty(PropertyState property); + + /** + * Set a property state + * @param name The name of this property + * @param value The value of this property + * @param The type of this property. Must be one of {@code String, Blob, byte[], Long, Integer, Double, Boolean, BigDecimal} + * @throws IllegalArgumentException if {@code T} is not one of the above types. + * + * @param name name of the property + * @return this builder + */ + NodeBuilder setProperty(String name, T value); + + /** + * Set a property state + * @param name The name of this property + * @param value The value of this property + * @param The type of this property. + * @return this builder + */ + NodeBuilder setProperty(String name, T value, Type type); + + /** + * Remove the named property. This method has no effect if a + * property of the given {@code name} does not exist. + * @param name name of the property + */ + @Nonnull + NodeBuilder removeProperty(String name); + + /** + * Returns a builder for constructing changes to the named child node. + * If the named child node does not already exist, a new empty child + * node is automatically created as the base state of the returned + * child builder. Otherwise the existing child node state is used + * as the base state of the returned builder. + *

    + * All updates to the returned child builder will implicitly affect + * also this builder, as if a + * {@code setNode(name, childBuilder.getNodeState())} method call + * had been made after each update. Repeated calls to this method with + * the same name will return the same child builder instance until an + * explicit {@link #setNode(String, NodeState)} or + * {@link #removeNode(String)} call is made, at which point the link + * between this builder and a previously returned child builder for + * that child node name will get broken. + * + * @since Oak 0.6 + * @param name name of the child node + * @return child builder + */ + @Nonnull + NodeBuilder child(String name); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/NodeState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/NodeState.java new file mode 100644 index 00000000000..9185709f7c5 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/NodeState.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.spi.state; + +import org.apache.jackrabbit.oak.api.PropertyState; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +/** + * A node in a content tree consists of child nodes and properties, each + * of which evolves through different states during its lifecycle. This + * interface represents a specific, immutable state of a node. The state + * consists of an unordered set of name -> item mappings, where + * each item is either a property or a child node. + *

    + * Depending on context, a NodeState instance can be interpreted as + * representing the state of just that node, of the subtree starting at + * that node, or of an entire tree in case it's a root node. + *

    + * The crucial difference between this interface and the similarly named + * class in Jackrabbit 2.x is that this interface represents a specific, + * immutable state of a node, whereas the Jackrabbit 2.x class represented + * the current state of a node. + * + *

    Immutability and thread-safety

    + *

    + * As mentioned above, all node and property states are always immutable. + * Thus repeating a method call is always guaranteed to produce the same + * result as before unless some internal error occurs (see below). This + * immutability only applies to a specific state instance. Different states + * of a node can obviously be different, and in some cases even different + * instances of the same state may behave slightly differently. For example + * due to performance optimization or other similar changes the iteration + * order of properties or child nodes may be different for two instances + * of the same state. + *

    + * In addition to being immutable, a specific state instance guaranteed to + * be fully thread-safe. Possible caching or other internal changes need to + * be properly synchronized so that any number of concurrent clients can + * safely access a state instance. + * + *

    Persistence and error-handling

    + *

    + * A node state can be (and often is) backed by local files or network + * resources. All IO operations or related concerns like caching should be + * handled transparently below this interface. Potential IO problems and + * recovery attempts like retrying a timed-out network access need to be + * handled below this interface, and only hard errors should be thrown up + * as {@link RuntimeException unchecked exceptions} that higher level code + * is not expected to be able to recover from. + *

    + * Since this interface exposes no higher level constructs like access + * controls, locking, node types or even path parsing, there's no way + * for content access to fail because of such concerns. Such functionality + * and related checked exceptions or other control flow constructs should + * be implemented on a higher level above this interface. + * + *

    Decoration and virtual content

    + *

    + * Not all content exposed by this interface needs to be backed by actual + * persisted data. An implementation may want to provide derived data, + * like for example the aggregate size of the entire subtree as an + * extra virtual property. A virtualization, sharding or caching layer + * could provide a composite view over multiple underlying trees. + * Or a basic access control layer could decide to hide certain content + * based on specific rules. All such features need to be implemented + * according to the API contract of this interface. A separate higher level + * interface needs to be used if an implementation can't for example + * guarantee immutability of exposed content as discussed above. + * + *

    Equality and hash codes

    + *

    + * Two node states are considered equal if and only if their properties and + * child nodes match, regardless of ordering. The + * {@link Object#equals(Object)} method needs to be implemented so that it + * complies with this definition. And while node states are not meant for + * use as hash keys, the {@link Object#hashCode()} method should still be + * implemented according to this equality contract. + */ +public interface NodeState { + + /** + * Returns the named property. The name is an opaque string and + * is not parsed or otherwise interpreted by this method. + *

    + * The namespace of properties and child nodes is shared, so if + * this method returns a non-{@code null} value for a given + * name, then {@link #getChildNode(String)} is guaranteed to return + * {@code null} for the same name. + * + * @param name name of the property to return + * @return named property, or {@code null} if not found + */ + @CheckForNull + PropertyState getProperty(String name); + + /** + * Returns the number of properties of this node. + * + * @return number of properties + */ + long getPropertyCount(); + + /** + * Returns an iterable of the properties of this node. Multiple + * iterations are guaranteed to return the properties in the same + * order, but the specific order used is implementation-dependent + * and may change across different states of the same node. + * + * @return properties in some stable order + */ + @Nonnull + Iterable getProperties(); + + /** + * Checks whether the named child node exists. + * + * @param name name of the child node + * @return {@code true} if the named child node exists, + * {@code false} otherwise + */ + boolean hasChildNode(String name); + + /** + * Returns the named child node. The name is an opaque string and + * is not parsed or otherwise interpreted by this method. + *

    + * The namespace of properties and child nodes is shared, so if + * this method returns a non-{@code null} value for a given + * name, then {@link #getProperty(String)} is guaranteed to return + * {@code null} for the same name. + * + * @param name name of the child node to return + * @return named child node, or {@code null} if not found + */ + @CheckForNull + NodeState getChildNode(String name); + + /** + * Returns the number of child nodes of this node. + * + * @return number of child nodes + */ + long getChildNodeCount(); + + /** + * Returns the names of all child nodes. + * + * @return child node names in some stable order + */ + Iterable getChildNodeNames(); + + /** + * Returns an iterable of the child node entries of this instance. Multiple + * iterations are guaranteed to return the child nodes in the same order, + * but the specific order used is implementation dependent and may change + * across different states of the same node. + *

    + * Note on cost and performance: while it is possible to iterate over + * all child NodeStates with the two methods {@link + * #getChildNodeNames()} and {@link #getChildNode(String)}, this method is + * considered more efficient because an implementation can potentially + * perform the retrieval of the name and NodeState in one call. + * This results in O(n) vs. O(n log n) when iterating over the child node + * names and then look up the NodeState by name. + * + * @return child node entries in some stable order + */ + @Nonnull + Iterable getChildNodeEntries(); + + /** + * Returns a builder for constructing a new node state based on + * this state, i.e. starting with all the properties and child nodes + * of this state. + * + * @since Oak 0.6 + * @return node builder based on this state + */ + @Nonnull + NodeBuilder builder(); + + /** + * Compares this node state against the given base state. Any differences + * are reported by calling the relevant added/changed/deleted methods of + * the given handler. + * + * @param base base state + * @param diff handler of node state differences + * @since 0ak 0.4 + */ + void compareAgainstBaseState(NodeState base, NodeStateDiff diff); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/NodeStateDiff.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/NodeStateDiff.java new file mode 100644 index 00000000000..d4898e6fff9 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/NodeStateDiff.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.state; + +import org.apache.jackrabbit.oak.api.PropertyState; + +/** + * Handler of node state differences. The + * {@link NodeState#compareAgainstBaseState(NodeState, NodeStateDiff)} + * method reports detected node state differences by calling methods of + * a handler instance that implements this interface. The compare method + * will go through all properties and child nodes of the two states, + * calling the relevant added, changed or deleted methods where appropriate. + * Differences in the ordering of properties or child nodes do not affect + * the comparison, and the order in which such differences are reported + * is unspecified. + *

    + * Note that the + * {@link NodeState#compareAgainstBaseState(NodeState, NodeStateDiff)} + * method only compares the given states without recursing to the subtrees + * below. An implementation of this interface should recursively call that + * method for the relevant child node entries to find out all the changes + * across the entire subtree below the given node. + */ +public interface NodeStateDiff { + + /** + * Called for all added properties. + * + * @param after property state after the change + */ + void propertyAdded(PropertyState after); + + /** + * Called for all changed properties. The names of the given two + * property states are guaranteed to be the same. + * + * @param before property state before the change + * @param after property state after the change + */ + void propertyChanged(PropertyState before, PropertyState after); + + /** + * Called for all deleted properties. + * + * @param before property state before the change + */ + void propertyDeleted(PropertyState before); + + /** + * Called for all added child nodes. + * + * @param name name of the added child node + * @param after child node state after the change + */ + void childNodeAdded(String name, NodeState after); + + /** + * Called for all changed child nodes. + * + * @param name name of the changed child node + * @param before child node state before the change + * @param after child node state after the change + */ + void childNodeChanged(String name, NodeState before, NodeState after); + + /** + * Called for all deleted child nodes. + * + * @param name name of the deleted child node + * @param before child node state before the change + */ + void childNodeDeleted(String name, NodeState before); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/NodeStateUtils.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/NodeStateUtils.java new file mode 100644 index 00000000000..252a4e4eca9 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/NodeStateUtils.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.spi.state; + +/** + * Utility method for code that deals with node states. + */ +public class NodeStateUtils { + + /** + * Check whether the node or property with the given name is hidden, that + * is, if the node name starts with a ":". + * + * @param name the node or property name + * @return true if the item is hidden + */ + public static boolean isHidden(String name) { + return name.startsWith(":"); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/NodeStore.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/NodeStore.java new file mode 100644 index 00000000000..b39546e206a --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/NodeStore.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.spi.state; + +import java.io.IOException; +import java.io.InputStream; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.Blob; + +/** + * Storage abstraction for trees. At any given point in time the stored + * tree is rooted at a single immutable node state. + *

    + * This is a low-level interface that doesn't cover functionality like + * merging concurrent changes or rejecting new tree states based on some + * higher-level consistency constraints. + */ +public interface NodeStore { + + /** + * Returns the latest state of the tree. + * + * @return root node state + */ + @Nonnull + NodeState getRoot(); + + /** + * Creates a new branch of the tree to which transient changes can be applied. + * + * @return branch + */ + @Nonnull + NodeStoreBranch branch(); + + /** + * Create a {@link Blob} from the given input stream. The input stream + * is closed after this method returns. + * @param inputStream The input stream for the {@code Blob} + * @return The {@code Blob} representing {@code inputStream} + * @throws IOException If an error occurs while reading from the stream + */ + Blob createBlob(InputStream inputStream) throws IOException; +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/NodeStoreBranch.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/NodeStoreBranch.java new file mode 100644 index 00000000000..e75d3fc932e --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/NodeStoreBranch.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.state; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.CommitFailedException; + +public interface NodeStoreBranch { + + /** + * Returns the base state of this branch. + * + * @return base node state + */ + @Nonnull + NodeState getBase(); + + /** + * Returns the latest state of the branch. + * + * @return root node state + */ + @Nonnull + NodeState getRoot(); + + /** + * Updates the state of the content tree. + * + * @param newRoot new root node state + */ + void setRoot(NodeState newRoot); + + /** + * Moves a node. + * + * @param source source path + * @param target target path + * @return {@code true} iff the move succeeded + */ + boolean move(String source, String target); + + /** + * Copies a node. + * + * @param source source path + * @param target target path + * @return {@code true} iff the copy succeeded + */ + boolean copy(String source, String target); + + /** + * Merges the changes in this branch to the main content tree. + * + * @return the node state resulting from the merge. + * @throws CommitFailedException if the merge failed + */ + @Nonnull + NodeState merge() throws CommitFailedException; + +} + diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/PropertyBuilder.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/PropertyBuilder.java new file mode 100644 index 00000000000..ae9ed248743 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/PropertyBuilder.java @@ -0,0 +1,171 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.state; + +import java.util.List; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.PropertyState; + +/** + * Builder interface for constructing new {@link PropertyState node states}. + */ +public interface PropertyBuilder { + + /** + * @return The name of the property state + */ + @CheckForNull + String getName(); + + /** + * @return The value of the property state or {@code null} if {@code isEmpty} is {@code true} + */ + @CheckForNull + T getValue(); + + /** + * @return A list of values of the property state + */ + @Nonnull + List getValues(); + + /** + * @param index + * @return The value of the property state at the given {@code index}. + * @throws IndexOutOfBoundsException if {@code index >= count} + */ + @Nonnull + T getValue(int index); + + /** + * @param value + * @return {@code true} iff the property state contains {@code value}. + */ + boolean hasValue(Object value); + + /** + * @return The number of values of the property state + */ + int count(); + + /** + * @return {@code true} iff {@code count() != 1} + */ + boolean isArray(); + + /** + * @return {{@code true}} iff {@code count() == 0} + * @return + */ + boolean isEmpty(); + + /** + * Returns an immutable property state that matches the current state of + * the builder. The {@code asArray} flag can be used to coerce a property + * state with a single value into a multi valued property state. + * Equivalent to {@code getPropertyState(false)} + * + * @return immutable property state + * @throws IllegalStateException If the name of the property is not set + */ + @Nonnull + PropertyState getPropertyState(); + + /** + * Returns an immutable property state that matches the current state of + * the builder. The {@code asArray} flag can be used to coerce a property + * state with a single value into a multi valued property state. + * + * @param asArray If {@code true} the builder creates a multi valued property state + * @return immutable property state + * @throws IllegalStateException If the name of the property is not set + */ + @Nonnull + PropertyState getPropertyState(boolean asArray); + + /** + * Clone {@code property} to the property state being built. After + * this call {@code getPropertyState(property.isArray()).equals(property)} will hold. + * @param property the property to clone + * @return {@code this} + */ + @Nonnull + PropertyBuilder assignFrom(PropertyState property); + + /** + * Set the name of the property + * @param name + * @return {@code this} + */ + @Nonnull + PropertyBuilder setName(String name); + + /** + * Set the value of the property state clearing all previously set values. + * @param value value to set + * @return {@code this} + */ + @Nonnull + PropertyBuilder setValue(T value); + + /** + * Add a value to the end of the list of values of the property state. + * @param value value to add + * @return {@code this} + */ + @Nonnull + PropertyBuilder addValue(T value); + + /** + * Set the value of the property state at the given {@code index}. + * @param value value to set + * @param index index to set the value + * @return {@code this} + * @throws IndexOutOfBoundsException if {@code index >= count} + */ + @Nonnull + PropertyBuilder setValue(T value, int index); + + /** + * Set the values of the property state clearing all previously set values. + * @param values + * @return {@code this} + */ + @Nonnull + PropertyBuilder setValues(Iterable values); + + /** + * Remove the value at the given {@code index} + * @param index + * @return {@code this} + * @throws IndexOutOfBoundsException if {@code index >= count} + */ + @Nonnull + PropertyBuilder removeValue(int index); + + /** + * Remove the given value from the property state + * @param value value to remove + * @return {@code this} + */ + @Nonnull + PropertyBuilder removeValue(Object value); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/ReadOnlyBuilder.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/ReadOnlyBuilder.java new file mode 100644 index 00000000000..8ce7a335477 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/ReadOnlyBuilder.java @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.state; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; + +/** + * A node builder that throws an {@link UnsupportedOperationException} on + * all attempts to modify the given base state. + */ +public class ReadOnlyBuilder implements NodeBuilder { + + private final NodeState state; + + public ReadOnlyBuilder(NodeState state) { + this.state = state; + } + + protected RuntimeException unsupported() { + return new UnsupportedOperationException("This builder is read-only."); + } + + @Override + public NodeState getNodeState() { + return state; + } + + @Override + public NodeState getBaseState() { + return state; + } + + @Override + public void reset(NodeState state) { + throw unsupported(); + } + + @Override + public long getChildNodeCount() { + return state.getChildNodeCount(); + } + + @Override + public boolean hasChildNode(String name) { + return state.hasChildNode(name); + } + + @Override + public Iterable getChildNodeNames() { + return state.getChildNodeNames(); + } + + @Override @Nonnull + public NodeBuilder setNode(String name, NodeState nodeState) { + throw unsupported(); + } + + @Override @Nonnull + public NodeBuilder removeNode(String name) { + throw unsupported(); + } + + @Override + public long getPropertyCount() { + return state.getPropertyCount(); + } + + @Override + public Iterable getProperties() { + return state.getProperties(); + } + + @Override + public PropertyState getProperty(String name) { + return state.getProperty(name); + } + + @Override @Nonnull + public NodeBuilder removeProperty(String name) { + throw unsupported(); + } + + @Override + public NodeBuilder setProperty(PropertyState property) { + throw unsupported(); + } + + @Override + public NodeBuilder setProperty(String name, T value) { + throw unsupported(); + } + + @Override + public NodeBuilder setProperty(String name, T value, Type type) { + throw unsupported(); + } + + @Override + public ReadOnlyBuilder child(String name) { + NodeState child = state.getChildNode(name); + if (child != null) { + return new ReadOnlyBuilder(child); + } else { + throw unsupported(); + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/NodeInfo.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/NodeInfo.java new file mode 100644 index 00000000000..45bb3210cf1 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/NodeInfo.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.xml; + +/** + * Information about a node being imported. This class is used + * by the XML import handlers to pass the parsed node information to the + * import process. + *

    + * An instance of this class is simply a container for the node name, + * node uuidentifier, and the node type information. See the {@link PropInfo} + * class for the related carrier of property information. + */ +public class NodeInfo { + + /** + * Name of the node being imported. + */ + private final String name; + + /** + * Name of the primary type of the node being imported. + */ + private final String primaryTypeName; + + /** + * Names of the mixin types of the node being imported. + */ + private final String[] mixinTypeNames; + + /** + * UUID of the node being imported. + */ + private final String uuid; + + /** + * Creates a node information instance. + * + * @param name name of the node being imported + * @param primaryTypeName name of the primary type of the node being imported + * @param mixinTypeNames names of the mixin types of the node being imported + * @param uuid uuid of the node being imported + */ + public NodeInfo(String name, String primaryTypeName, String[] mixinTypeNames, + String uuid) { + this.name = name; + this.primaryTypeName = primaryTypeName; + this.mixinTypeNames = mixinTypeNames; + this.uuid = uuid; + } + + /** + * Returns the name of the node being imported. + * + * @return node name + */ + public String getString() { + return name; + } + + /** + * Returns the name of the primary type of the node being imported. + * + * @return primary type name + */ + public String getPrimaryTypeName() { + return primaryTypeName; + } + + /** + * Returns the names of the mixin types of the node being imported. + * + * @return mixin type names + */ + public String[] getMixinTypeNames() { + return mixinTypeNames; + } + + /** + * Returns the uuid of the node being imported. + * + * @return node uuid + */ + public String getUUID() { + return uuid; + } + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/PropInfo.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/PropInfo.java new file mode 100644 index 00000000000..010e90980e0 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/PropInfo.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.xml; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; + +/** + * Information about a property being imported. This class is used + * by the XML import handlers to pass the parsed property information + * to the import process. + *

    + * In addition to carrying the actual property data, instances of this + * class also know how to apply that data when imported either to a + * {@link javax.jcr.Node} instance through a session or directly to a + * {@link org.apache.jackrabbit.oak.api.Tree} instance on the oak level. + */ +public class PropInfo { + + /** + * String of the property being imported. + */ + private final String name; + + /** + * Type of the property being imported. + */ + private final Type type; + + /** + * Value(s) of the property being imported. + */ + private final TextValue[] values; + + /** + * Hint indicating whether the property is multi- or single-value + */ + public enum MultipleStatus { UNKNOWN, SINGLE, MULTIPLE } + private MultipleStatus multipleStatus; + + /** + * Creates a property information instance. + * + * @param name name of the property being imported + * @param type type of the property being imported + * @param values value(s) of the property being imported + */ + public PropInfo(String name, Type type, TextValue[] values) { + this.name = name; + this.type = type; + this.values = values; + multipleStatus = (values.length == 1) ? MultipleStatus.UNKNOWN : MultipleStatus.MULTIPLE; + } + + /** + * Creates a property information instance. + * + * @param name name of the property being imported + * @param type type of the property being imported + * @param values value(s) of the property being imported + * @param multipleStatus Hint indicating whether the property is + * multi- or single-value + */ + public PropInfo(String name, Type type, TextValue[] values, + MultipleStatus multipleStatus) { + this.name = name; + this.type = type; + this.values = values; + this.multipleStatus = multipleStatus; + } + + /** + * Disposes all values contained in this property. + */ + public void dispose() { + for (TextValue value : values) { + value.dispose(); + } + } + + public Type getTargetType(PropertyDefinition def) { + int target = def.getRequiredType(); + if (target != PropertyType.UNDEFINED) { + return Type.fromTag(target, def.isMultiple()); + } else if (type.tag() != PropertyType.UNDEFINED) { + return type; + } else { + return Type.STRING; + } + } + + public String getString() { + return name; + } + + public Type getType() { + return type; + } + + public TextValue[] getTextValues() { + return values; + } + + public Value[] getValues(Type targetType, NamePathMapper namePathMapper) throws RepositoryException { + Value[] va = new Value[values.length]; + for (int i = 0; i < values.length; i++) { + va[i] = values[i].getValue(targetType, namePathMapper); + } + return va; + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/ProtectedItemImporter.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/ProtectedItemImporter.java new file mode 100644 index 00000000000..ce9d81f0db4 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/ProtectedItemImporter.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.xml; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; + +/** + * Base interface for {@link ProtectedNodeImporter} and {@link ProtectedPropertyImporter}. + */ +public abstract interface ProtectedItemImporter { + + /** + * Initializes the importer. + * + * @param session The session that is running the import. + * @param root The root associated with the import. + * @param namePathMapper The name/path mapper used to translate names + * between their jcr and oak form. + * @param isWorkspaceImport A flag indicating whether the import has been + * started from the {@link javax.jcr.Workspace} or from the + * {@link javax.jcr.Session}. Implementations are free to implement both + * types of imports or only a single one. For example it doesn't make sense + * to allow for importing versions along with a Session import as + * version operations are required to never leave transient changes behind. + * @param uuidBehavior The uuid behavior specified with the import call. + * @param referenceTracker The uuid/reference helper. + * @return {@code true} if this importer was successfully initialized and + * is able to handle an import with the given setup; {@code false} otherwise. + */ + boolean init(JackrabbitSession session, Root root, + NamePathMapper namePathMapper, + boolean isWorkspaceImport, int uuidBehavior, + ReferenceChangeTracker referenceTracker); + + /** + * Post processing protected reference properties underneath a protected + * or non-protected parent node. If the parent is protected it has been + * handled by this importer already. + * + * @throws javax.jcr.RepositoryException If an error occurs. + */ + void processReferences() throws RepositoryException; +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/ProtectedNodeImporter.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/ProtectedNodeImporter.java new file mode 100644 index 00000000000..b2f360f4691 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/ProtectedNodeImporter.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.xml; + +import java.util.List; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.oak.api.Tree; + +/** + * {@code ProtectedNodeImporter} provides means to import protected + * {@code Node}s and the subtree defined below such nodes. + *

    + * The import of a protected tree is started by the {@code Importer} by + * calling {@link #start(Tree)}. If the {@code ProtectedNodeImporter} + * is able to deal with that type of protected node, it is in charge of dealing + * with all subsequent child {@code NodeInfo}s present below the protected + * parent until {@link #end(Tree)} is called. The latter resets this importer + * and makes it available for another protected import. + */ +public interface ProtectedNodeImporter extends ProtectedItemImporter { + + + /** + * Notifies this importer about the existence of a protected node that + * has either been created (NEW) or has been found to be existing. + * This importer implementation is in charge of evaluating the nature of + * that protected node in order to determine, if it is able to handle + * subsequent protected or non-protected child nodes in the tree below + * that parent. + * + * @param protectedParent A protected node that has either been created + * during the current XML import or that has been found to be existing + * without allowing same-name siblings. + * @return {@code true} If this importer is able to deal with the + * tree that may be present below the given protected Node. + * @throws IllegalStateException If this method is called on + * this importer without having reached {@link #end(Tree)}. + * @throws javax.jcr.RepositoryException If an error occurs. + */ + boolean start(Tree protectedParent) throws IllegalStateException, + RepositoryException; + + /** + * Informs this importer that the tree to be imported below + * {@code protectedParent} has bee completed. This allows the importer + * to be reset in order to be able to deal with another call to + * {@link #start(Tree)}.

    + * If {@link #start(Tree)} hasn't been called before, this method returns + * silently. + * + * @param protectedParent The protected parent tree. + * @throws IllegalStateException If end is called in an illegal state. + * @throws javax.jcr.nodetype.ConstraintViolationException If the tree + * that was imported is incomplete. + * @throws RepositoryException If another error occurs. + */ + void end(Tree protectedParent) throws IllegalStateException, + ConstraintViolationException, RepositoryException; + + /** + * Informs this importer about a new {@code childInfo} and it's properties. + * If the importer is able to successfully import the given information + * this method returns silently. Otherwise + * {@code ConstraintViolationException} is thrown, in which case the + * whole import fails.

    + * In case this importer deals with multiple levels of nodes, it is in + * charge of maintaining the hierarchical structure (see also {#link endChildInfo()}. + *

    + * If {@link #start(Tree)} hasn't been called before, this method returns + * silently. + * + * @param childInfo + * @param propInfos + * @throws IllegalStateException If called in an illegal state. + * @throws javax.jcr.nodetype.ConstraintViolationException If the given + * infos contain invalid or incomplete data and therefore cannot be properly + * handled by this importer. + * @throws RepositoryException If another error occurs. + */ + void startChildInfo(NodeInfo childInfo, List propInfos) + throws IllegalStateException, ConstraintViolationException, RepositoryException; + + /** + * Informs this importer about the end of a child info. + *

    + * If {@link #start(Tree)} hasn't been called before, this method returns + * silently. + * + * @throws IllegalStateException If end is called in an illegal state. + * @throws javax.jcr.nodetype.ConstraintViolationException If this method + * is called before all required child information has been imported. + * @throws RepositoryException If another error occurs. + */ + void endChildInfo() throws RepositoryException; +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/ProtectedPropertyImporter.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/ProtectedPropertyImporter.java new file mode 100644 index 00000000000..2b146df3b6f --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/ProtectedPropertyImporter.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.xml; + +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.oak.api.Tree; + +/** + * {@code ProtectedPropertyImporter} is in charge of importing single + * properties with a protected {@code PropertyDefinition}. + * + * @see ProtectedNodeImporter for an abstract class used to import protected + * nodes and the subtree below them. + */ +public interface ProtectedPropertyImporter extends ProtectedItemImporter { + + /** + * Handles a single protected property. + * + * @param parent The affected parent node. + * @param protectedPropInfo The {@code PropInfo} to be imported. + * @param def The property definition determined by the importer that + * calls this method. + * @return {@code true} If the property could be successfully imported; + * {@code false} otherwise. + * @throws javax.jcr.RepositoryException If an error occurs. + */ + boolean handlePropInfo(Tree parent, PropInfo protectedPropInfo, + PropertyDefinition def) throws RepositoryException; + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/ReferenceChangeTracker.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/ReferenceChangeTracker.java new file mode 100644 index 00000000000..a999135a6e6 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/ReferenceChangeTracker.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.spi.xml; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Helper class used to keep track of uuid mappings (e.g. if the uuid of an + * imported or copied node is mapped to a new uuid) and processed (e.g. imported + * or copied) reference properties that might need to be adjusted depending on + * the UUID mapping resulting from the import. + * + * @see javax.jcr.ImportUUIDBehavior + */ +public class ReferenceChangeTracker { + + /** + * mapping from original uuid to new uuid of mix:referenceable nodes + */ + private final Map uuidMap = new HashMap(); + + /** + * list of processed reference properties that might need correcting + */ + private final ArrayList references = new ArrayList(); + + /** + * Returns the new node id to which {@code oldUUID} has been mapped + * or {@code null} if no such mapping exists. + * + * @param oldUUID old node id + * @return mapped new id or {@code null} if no such mapping exists + * @see #put(String, String) + */ + public String get(String oldUUID) { + return uuidMap.get(oldUUID); + } + + /** + * Store the given id mapping for later lookup using + * {@code }{@link #get(String)}. + * + * @param oldUUID old node id + * @param newUUID new node id + */ + public void put(String oldUUID, String newUUID) { + uuidMap.put(oldUUID, newUUID); + } + + /** + * Resets all internal state. + */ + public void clear() { + uuidMap.clear(); + references.clear(); + } + + /** + * Store the given reference property for later retrieval using + * {@code }{@link #getProcessedReferences()}. + * + * @param refProp reference property + */ + public void processedReference(Object refProp) { + references.add(refProp); + } + + /** + * Returns an iterator over all processed reference properties. + * + * @return an iterator over all processed reference properties + * @see #processedReference(Object) + */ + public Iterator getProcessedReferences() { + return references.iterator(); + } + + /** + * Remove the given references that have already been processed from the + * references list. + * + * @param processedReferences + * @return {@code true} if the internal list of references changed. + */ + public boolean removeReferences(List processedReferences) { + return references.removeAll(processedReferences); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/TextValue.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/TextValue.java new file mode 100644 index 00000000000..de36bc9774f --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/xml/TextValue.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.spi.xml; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; + +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; + +/** + * {@code TextValue} represents a serialized property value read + * from a System or Document View XML document. + */ +public interface TextValue { + + // TODO: review again + Value getValue(Type targetType, NamePathMapper namePathMapper) throws ValueFormatException, RepositoryException; + + /** + * Dispose this value, i.e. free all bound resources. Once a value has + * been disposed, further method invocations will cause an IOException + * to be thrown. + */ + void dispose(); + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/ArrayUtils.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/util/ArrayUtils.java similarity index 96% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/util/ArrayUtils.java rename to oak-core/src/main/java/org/apache/jackrabbit/oak/util/ArrayUtils.java index 8fe6ef7b383..eeb3bbf1cc4 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/ArrayUtils.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/util/ArrayUtils.java @@ -14,10 +14,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.util; +package org.apache.jackrabbit.oak.util; import java.lang.reflect.Array; +import javax.annotation.Nonnull; + /** * Array utility methods. */ @@ -27,6 +29,10 @@ public class ArrayUtils { public static final long[] EMPTY_LONG_ARRAY = new long[0]; public static final int[] EMPTY_INTEGER_ARRAY = new int[0]; + private ArrayUtils() { + // utility class + } + /** * Replace an element in a clone of the array at the given position. * @@ -35,6 +41,7 @@ public class ArrayUtils { * @param x the value to add * @return the new array */ + @Nonnull public static T[] arrayReplace(T[] values, int index, T x) { int size = values.length; @SuppressWarnings("unchecked") @@ -84,6 +91,7 @@ public static long[] arrayInsert(long[] values, int index, long x) { * @param x the value to add * @return the new array */ + @Nonnull public static T[] arrayInsert(T[] values, int index, T x) { int size = values.length; @SuppressWarnings("unchecked") @@ -101,6 +109,7 @@ public static T[] arrayInsert(T[] values, int index, T x) { * @param x the value to add * @return the new array */ + @Nonnull public static String[] arrayInsert(String[] values, int index, String x) { int size = values.length; String[] v2 = new String[size + 1]; @@ -133,6 +142,7 @@ public static int[] arrayRemove(int[] values, int index) { * @param index the index * @return the new array */ + @Nonnull public static T[] arrayRemove(T[] values, int index) { int size = values.length; @SuppressWarnings("unchecked") @@ -166,6 +176,7 @@ public static long[] arrayRemove(long[] values, int index) { * @param index the index * @return the new array */ + @Nonnull public static String[] arrayRemove(String[] values, int index) { int size = values.length; if (size == 1) { diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/util/NodeUtil.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/util/NodeUtil.java new file mode 100644 index 00000000000..21b0ace87ce --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/util/NodeUtil.java @@ -0,0 +1,253 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.util; + +import java.util.Arrays; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.List; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import org.apache.jackrabbit.JcrConstants; +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.namepath.NameMapper; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.plugins.memory.PropertyStates; +import org.apache.jackrabbit.oak.plugins.value.Conversions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.jackrabbit.oak.api.Type.BOOLEAN; +import static org.apache.jackrabbit.oak.api.Type.DATE; +import static org.apache.jackrabbit.oak.api.Type.LONG; +import static org.apache.jackrabbit.oak.api.Type.NAME; +import static org.apache.jackrabbit.oak.api.Type.NAMES; +import static org.apache.jackrabbit.oak.api.Type.STRING; +import static org.apache.jackrabbit.oak.api.Type.STRINGS; + +/** + * Utility class for accessing and writing typed content of a tree. + */ +public class NodeUtil { + + private static final Logger log = LoggerFactory.getLogger(NodeUtil.class); + + private final NameMapper mapper; + + private final Tree tree; + + public NodeUtil(Tree tree, NameMapper mapper) { + this.mapper = mapper; + this.tree = tree; + } + + public NodeUtil(Tree tree) { + this(tree, NamePathMapper.DEFAULT); + } + + @Nonnull + public Tree getTree() { + return tree; + } + + @Nonnull + public String getName() { + return mapper.getJcrName(tree.getName()); + } + + public NodeUtil getParent() { + return new NodeUtil(tree.getParent(), mapper); + } + + public boolean hasChild(String name) { + return tree.getChild(name) != null; + } + + @CheckForNull + public NodeUtil getChild(String name) { + Tree child = tree.getChild(name); + return (child == null) ? null : new NodeUtil(child, mapper); + } + + @Nonnull + public NodeUtil addChild(String name, String primaryNodeTypeName) { + Tree child = tree.addChild(name); + NodeUtil childUtil = new NodeUtil(child, mapper); + childUtil.setName(JcrConstants.JCR_PRIMARYTYPE, primaryNodeTypeName); + return childUtil; + } + + public NodeUtil getOrAddChild(String name, String primaryTypeName) { + NodeUtil child = getChild(name); + return (child != null) ? child : addChild(name, primaryTypeName); + } + + public boolean hasPrimaryNodeTypeName(String ntName) { + return ntName.equals(getString(JcrConstants.JCR_PRIMARYTYPE, null)); + } + + public void removeProperty(String name) { + tree.removeProperty(name); + } + + public boolean getBoolean(String name) { + PropertyState property = tree.getProperty(name); + return property != null && !property.isArray() + && property.getValue(BOOLEAN); + } + + public void setBoolean(String name, boolean value) { + tree.setProperty(name, value); + } + + public String getString(String name, String defaultValue) { + PropertyState property = tree.getProperty(name); + if (property != null && !property.isArray()) { + return property.getValue(Type.STRING); + } else { + return defaultValue; + } + } + + public void setString(String name, String value) { + tree.setProperty(name, value); + } + + public String[] getStrings(String name) { + PropertyState property = tree.getProperty(name); + if (property == null) { + return null; + } + + return Iterables.toArray(property.getValue(STRINGS), String.class); + } + + public void setStrings(String name, String... values) { + tree.setProperty(name, Arrays.asList(values), STRINGS); + } + + public String getName(String name) { + return getName(name, null); + } + + public String getName(String name, String defaultValue) { + PropertyState property = tree.getProperty(name); + if (property != null && !property.isArray()) { + return mapper.getJcrName(property.getValue(STRING)); + } else { + return defaultValue; + } + } + + public void setName(String name, String value) { + String oakName = getOakName(value); + tree.setProperty(name, oakName, NAME); + } + + public String[] getNames(String name, String... defaultValues) { + String[] strings = getStrings(name); + if (strings == null) { + strings = defaultValues; + } + for (int i = 0; i < strings.length; i++) { + strings[i] = mapper.getJcrName(strings[i]); + } + return strings; + } + + public void setNames(String name, String... values) { + tree.setProperty(name, Arrays.asList(values), NAMES); + } + + public void setDate(String name, long time) { + Calendar cal = GregorianCalendar.getInstance(); + cal.setTimeInMillis(time); + tree.setProperty(name, Conversions.convert(cal).toDate(), DATE); + } + + public long getLong(String name, long defaultValue) { + PropertyState property = tree.getProperty(name); + if (property != null && !property.isArray()) { + return property.getValue(LONG); + } else { + return defaultValue; + } + } + + public void setLong(String name, long value) { + tree.setProperty(name, value); + } + + public List getNodes(String namePrefix) { + List nodes = Lists.newArrayList(); + for (Tree child : tree.getChildren()) { + if (child.getName().startsWith(namePrefix)) { + nodes.add(new NodeUtil(child, mapper)); + } + } + return nodes; + } + + public void setValues(String name, Value[] values) { + try { + tree.setProperty(PropertyStates.createProperty(name, Arrays.asList(values))); + } + catch (RepositoryException e) { + log.warn("Unable to convert a default value", e); + } + } + + public void setValues(String name, String[] values, int type) { + tree.setProperty(name, Arrays.asList(values), STRINGS); + } + + public Value[] getValues(String name, ValueFactory vf) { + PropertyState property = tree.getProperty(name); + if (property != null) { + int type = property.getType().tag(); + List values = Lists.newArrayList(); + for (String value : property.getValue(STRINGS)) { + try { + values.add(vf.createValue(value, type)); + } catch (RepositoryException e) { + log.warn("Unable to convert a default value", e); + } + } + return values.toArray(new Value[values.size()]); + } else { + return null; + } + } + + private String getOakName(String jcrName) { + String oakName = mapper.getOakName(jcrName); + if (oakName == null) { + throw new IllegalArgumentException(new RepositoryException("Invalid name:" + jcrName)); + } + return oakName; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/util/TODO.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/util/TODO.java new file mode 100644 index 00000000000..6065e476bb4 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/util/TODO.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.util; + +import java.util.concurrent.Callable; + +import javax.jcr.UnsupportedRepositoryOperationException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Helper class for identifying partially implemented features and + * controlling their runtime behavior. + * + * @see OAK-193 + */ +public class TODO { + + private static final String mode = System.getProperty("todo", "strict"); + + private static boolean strict = "strict".equals(mode); + + private static boolean log = "log".equals(mode); + + public static void relax() { + strict = false; + log = true; + } + + public static TODO unimplemented() { + return new TODO("unimplemented"); + } + + public static TODO dummyImplementation() { + return new TODO("dummy implementation"); + } + + private final UnsupportedOperationException exception; + + private final Logger logger; + + private final String message; + + private TODO(String message) { + this.exception = new UnsupportedOperationException(message); + StackTraceElement[] trace = exception.getStackTrace(); + if (trace != null && trace.length > 2) { + String className = trace[2].getClassName(); + String methodName = trace[2].getMethodName(); + this.logger = LoggerFactory.getLogger(className); + this.message = + "TODO: " + className + "." + methodName + "() - " + message; + } else { + this.logger = LoggerFactory.getLogger(TODO.class); + this.message = "TODO: " + message; + } + } + + public void doNothing() throws UnsupportedRepositoryOperationException { + if (strict) { + throw exception(); + } else if (log) { + logger.warn(message, exception); + } + } + + public UnsupportedRepositoryOperationException exception() { + return new UnsupportedRepositoryOperationException(message, exception); + } + + public T returnValue(final T value) + throws UnsupportedRepositoryOperationException { + return call(new Callable() { + @Override + public T call() { + return value; + } + }); + } + + public T call(Callable callable) + throws UnsupportedRepositoryOperationException { + if (strict) { + throw exception(); + } else if (log) { + logger.warn(message, exception); + } + try { + return callable.call(); + } catch (Exception e) { + throw new UnsupportedRepositoryOperationException( + message + " failure", e); + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/version/VersionConstants.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/version/VersionConstants.java new file mode 100644 index 00000000000..8673416f2e8 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/version/VersionConstants.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.version; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import org.apache.jackrabbit.JcrConstants; + +/** + * VersionConstants... TODO + */ +public interface VersionConstants extends JcrConstants { + + // activities + String JCR_ACTIVITY = "jcr:activity"; + String JCR_ACTIVITIES = "jcr:activities"; + String JCR_ACTIVITY_TITLE = "jcr:activityTitle"; + String NT_ACTIVITY = "nt:activity"; + String REP_ACTIVITIES = "rep:Activities"; + + // configurations + String JCR_CONFIGURATION = "jcr:configuration"; + String JCR_CONFIGURATIONS = "jcr:configurations"; + String JCR_ROOT = "jcr:root"; // TODO: possible collisions? + String NT_CONFIGURATION = "nt:configuration"; + String REP_CONFIGURATIONS = "rep:Configurations"; + + // nt:versionHistory + String JCR_COPIED_FROM = "jcr:copiedFrom"; + + // nt:versionedChild + String JCR_CHILD_VERSION_HISTORY = "jcr:childVersionHistory"; + + /** + * Quote from JSR 283 Section "15.12.3 Activity Storage"

    + * + * Activities are persisted as nodes of type nt:activity under system-generated + * node names in activity storage below /jcr:system/jcr:activities.
    + * Similar to the /jcr:system/jcr:versionStorage subgraph, the activity storage + * is a single repository wide store, but is reflected into each workspace. + */ + String ACTIVITIES_PATH = '/' + JCR_SYSTEM + '/' + JCR_ACTIVITIES; + + /** + * Quote from JSR 283 Section "15.13.2 Configuration Proxy Nodes"

    + * + * Each configuration in a given workspace is represented by a distinct proxy + * node of type nt:configuration located in configuration storage within the + * same workspace under /jcr:system/jcr:configurations/. The configuration + * storage in a particular workspace is specific to that workspace. It is + * not a common repository-wide store mirrored into each workspace, as is + * the case with version storage. + */ + String CONFIGURATIONS_PATH = '/' + JCR_SYSTEM + '/' + JCR_CONFIGURATIONS; + + /** + * Quote from JSR 283 Section "3.13.8 Version Storage"

    + * + * Version histories are stored in a single, repository-wide version storage + * mutable and readable through the versioning API. + * Under full versioning the version storage data must, additionally, be + * reflected in each workspace as a protected subgraph [...] located below + * /jcr:system/jcr:versionStorage. + */ + String VERSION_STORE_PATH = '/' + JCR_SYSTEM + '/' + JCR_VERSIONSTORAGE; + + Collection SYSTEM_PATHS = Collections.unmodifiableList(Arrays.asList( + ACTIVITIES_PATH, + CONFIGURATIONS_PATH, + VERSION_STORE_PATH + )); + + Collection VERSION_PROPERTY_NAMES = Collections.unmodifiableList(Arrays.asList( + JCR_ACTIVITY, + JCR_ACTIVITY_TITLE, + JCR_BASEVERSION, + JCR_CHILD_VERSION_HISTORY, + JCR_CONFIGURATION, + JCR_COPIED_FROM, + JCR_FROZENMIXINTYPES, + JCR_FROZENPRIMARYTYPE, + JCR_FROZENUUID, + JCR_ISCHECKEDOUT, + JCR_MERGEFAILED, + JCR_PREDECESSORS, + JCR_ROOT, + JCR_SUCCESSORS, + JCR_VERSIONABLEUUID, + JCR_VERSIONHISTORY + )); + + Collection VERSION_NODE_NAMES = Collections.unmodifiableList(Arrays.asList( + JCR_ACTIVITIES, + JCR_CONFIGURATIONS, + JCR_FROZENNODE, + JCR_ROOTVERSION, + JCR_VERSIONLABELS + )); + + Collection VERSION_NODE_TYPE_NAMES = Collections.unmodifiableList(Arrays.asList( + NT_ACTIVITY, + NT_CONFIGURATION, + NT_FROZENNODE, + NT_VERSION, + NT_VERSIONEDCHILD, + NT_VERSIONHISTORY, + NT_VERSIONLABELS, + REP_ACTIVITIES, + REP_CONFIGURATIONS + )); +} \ No newline at end of file diff --git a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/footer.js b/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/footer.js deleted file mode 100644 index f4585e94c55..00000000000 --- a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/footer.js +++ /dev/null @@ -1,2 +0,0 @@ -document.write('

    '); - diff --git a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/getHeadRevision.html b/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/getHeadRevision.html deleted file mode 100644 index 50761716146..00000000000 --- a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/getHeadRevision.html +++ /dev/null @@ -1,23 +0,0 @@ - - -µKernel prototype: getHeadRevision - - - - - - -
    -

    Revision Operations: getHeadRevision

    -
    - -
      -
    -
    -

    - -

    - -
    - - diff --git a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/header.js b/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/header.js deleted file mode 100644 index 60f0e17f71f..00000000000 --- a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/header.js +++ /dev/null @@ -1 +0,0 @@ -document.write('
    µKernel
    prototype
    '); diff --git a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/index.html b/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/index.html deleted file mode 100644 index 1463a6fdd2b..00000000000 --- a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/index.html +++ /dev/null @@ -1,34 +0,0 @@ - - -µKernel prototype - - - - - - -
    -

    -

    Revision Operations

    - getHeadRevision
    - getRevisions
    - waitForCommit
    - getJournal
    - diff
    - -

    Read Operations

    - nodeExists
    - getNodes
    - -

    Write Operations

    - commit
    - -

    Blob Operations

    - getLength
    - read
    - write
    -

    -
    - - - diff --git a/oak-core/src/main/resources/org/apache/jackrabbit/oak/plugins/nodetype/builtin_nodetypes.cnd b/oak-core/src/main/resources/org/apache/jackrabbit/oak/plugins/nodetype/builtin_nodetypes.cnd new file mode 100644 index 00000000000..37949f2d877 --- /dev/null +++ b/oak-core/src/main/resources/org/apache/jackrabbit/oak/plugins/nodetype/builtin_nodetypes.cnd @@ -0,0 +1,678 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + + + + + + +//------------------------------------------------------------------------------ +// B A S E T Y P E +//------------------------------------------------------------------------------ + +/** + * nt:base is an abstract primary node type that is the base type for all other + * primary node types. It is the only primary node type without supertypes. + * + * @since 1.0 + */ +[nt:base] + abstract + - jcr:primaryType (NAME) mandatory autocreated protected COMPUTE + - jcr:mixinTypes (NAME) protected multiple COMPUTE + +//------------------------------------------------------------------------------ +// S T A N D A R D A P P L I C A T I O N N O D E T Y P E S +//------------------------------------------------------------------------------ + +/** + * This abstract node type serves as the supertype of nt:file and nt:folder. + * @since 1.0 + */ +[nt:hierarchyNode] > mix:created + abstract + +/** + * Nodes of this node type may be used to represent files. This node type inherits + * the item definitions of nt:hierarchyNode and requires a single child node called + * jcr:content. The jcr:content node is used to hold the actual content of the + * file. This child node is mandatory, but not auto-created. Its node type will be + * application-dependent and therefore it must be added by the user. A common + * approach is to make the jcr:content a node of type nt:resource. The + * jcr:content child node is also designated as the primary child item of its parent. + * + * @since 1.0 + */ +[nt:file] > nt:hierarchyNode + primaryitem jcr:content + + jcr:content (nt:base) mandatory + +/** + * The nt:linkedFile node type is similar to nt:file, except that the content + * node is not stored directly as a child node, but rather is specified by a + * REFERENCE property. This allows the content node to reside anywhere in the + * workspace and to be referenced by multiple nt:linkedFile nodes. The content + * node must be referenceable. + * + * @since 1.0 + */ +[nt:linkedFile] > nt:hierarchyNode + primaryitem jcr:content + - jcr:content (REFERENCE) mandatory + +/** + * Nodes of this type may be used to represent folders or directories. This node + * type inherits the item definitions of nt:hierarchyNode and adds the ability + * to have any number of other nt:hierarchyNode child nodes with any names. + * This means, in particular, that it can have child nodes of types nt:folder, + * nt:file or nt:linkedFile. + * + * @since 1.0 + */ +[nt:folder] > nt:hierarchyNode + + * (nt:hierarchyNode) VERSION + +/** + * This node type may be used to represent the content of a file. In particular, + * the jcr:content subnode of an nt:file node will often be an nt:resource. + * + * @since 1.0 + */ +[nt:resource] > mix:mimeType, mix:lastModified, mix:referenceable + primaryitem jcr:data + - jcr:data (BINARY) mandatory + +/** + * This mixin node type can be used to add standardized title and description + * properties to a node. + * + * Note that the protected attributes suggested by JSR283 are omitted in this variant. + * @since 2.0 + */ +[mix:title] + mixin + - jcr:title (STRING) + - jcr:description (STRING) + +/** + * This mixin node type can be used to add standardized creation information + * properties to a node. Since the properties are protected, their values are + * controlled by the repository, which should set them appropriately upon the + * initial persist of a node with this mixin type. In cases where this mixin is + * added to an already existing node the semantics of these properties are + * implementation specific. Note that jackrabbit initializes the properties to + * the current date and user in this case. + * + * + * @since 2.0 + */ +[mix:created] + mixin + - jcr:created (DATE) autocreated protected + - jcr:createdBy (STRING) autocreated protected + +/** + * This mixin node type can be used to provide standardized modification + * information properties to a node. + * + * The following is not yet implemented in Jackrabbit: + * "Since the properties are protected, their values + * are controlled by the repository, which should set them appropriately upon a + * significant modification of the subgraph of a node with this mixin. What + * constitutes a significant modification will depend on the semantics of the various + * parts of a node's subgraph and is implementation-dependent" + * + * Jackrabbit initializes the properties to the current date and user in the + * case they are newly created. + * + * Note that the protected attributes suggested by JSR283 are omitted in this variant. + * @since 2.0 + */ +[mix:lastModified] + mixin + - jcr:lastModified (DATE) autocreated + - jcr:lastModifiedBy (STRING) autocreated + +/** + * This mixin node type can be used to provide a standardized property that + * specifies the natural language in which the content of a node is expressed. + * The value of the jcr:language property should be a language code as defined + * in RFC 46465. Examples include "en" (English), "en-US" (United States English), + * "de" (German) and "de-CH" (Swiss German). + * + * Note that the protected attributes suggested by JSR283 are omitted in this variant. + * @since 2.0 + */ +[mix:language] + mixin + - jcr:language (STRING) + +/** + * This mixin node type can be used to provide standardized mimetype and + * encoding properties to a node. If a node of this type has a primary item + * that is a single-value BINARY property then jcr:mimeType property indicates + * the media type applicable to the contents of that property and, if that + * media type is one to which a text encoding applies, the jcr:encoding property + * indicates the character set used. If a node of this type does not meet the + * above precondition then the interpretation of the jcr:mimeType and + * jcr:encoding properties is implementation-dependent. + * + * Note that the protected attributes suggested by JSR283 are omitted in this variant. + * @since 2.0 + */ +[mix:mimeType] + mixin + - jcr:mimeType (STRING) + - jcr:encoding (STRING) + +/** + * This node type may be used to represent the location of a JCR item not just + * within a particular workspace but within the space of all workspaces in all JCR + * repositories. + * + * @prop jcr:protocol Stores a string holding the protocol through which the + * target repository is to be accessed. + * @prop jcr:host Stores a string holding the host name of the system + * through which the repository is to be accessed. + * @prop jcr:port Stores a string holding the port number through which the + * target repository is to be accessed. + * + * The semantics of these properties above are left undefined but are assumed to be + * known by the application. The names and descriptions of the properties are not + * normative and the repository does not enforce any particular semantic + * interpretation on them. + * + * @prop jcr:repository Stores a string holding the name of the target repository. + * @prop jcr:workspace Stores the name of a workspace. + * @prop jcr:path Stores a path to an item. + * @prop jcr:id Stores a weak reference to a node. + * + * In most cases either the jcr:path or the jcr:id property would be used, but + * not both, since they may point to different nodes. If any of the properties + * other than jcr:path and jcr:id are missing, the address can be interpreted as + * relative to the current container at the same level as the missing specifier. + * For example, if no repository is specified, then the address can be + * interpreted as referring to a workspace and path or id within the current + * repository. + * + * @since 2.0 + */ +[nt:address] + - jcr:protocol (STRING) + - jcr:host (STRING) + - jcr:port (STRING) + - jcr:repository (STRING) + - jcr:workspace (STRING) + - jcr:path (PATH) + - jcr:id (WEAKREFERENCE) + +/** + * The mix:etag mixin type defines a standardized identity validator for BINARY + * properties similar to the entity tags used in HTTP/1.1 + * + * A jcr:etag property is an opaque string whose syntax is identical to that + * defined for entity tags in HTTP/1.1. Semantically, the jcr:etag is comparable + * to the HTTP/1.1 strong entity tag. + * + * On creation of a mix:etag node N, or assignment of mix:etag to N, the + * repository must create a jcr:etag property with an implementation determined + * value. + * + * The value of the jcr:etag property must change immediately on persist of any + * of the following changes to N: + * - A BINARY property is added t o N. + * - A BINARY property is removed from N. + * - The value of an existing BINARY property of N changes. + * + * @since 2.0 + */ +[mix:etag] + mixin + - jcr:etag (STRING) protected autocreated + +//------------------------------------------------------------------------------ +// U N S T R U C T U R E D C O N T E N T +//------------------------------------------------------------------------------ + +/** + * This node type is used to store unstructured content. It allows any number of + * child nodes or properties with any names. It also allows multiple nodes having + * the same name as well as both multi-value and single-value properties with any + * names. This node type also supports client-orderable child nodes. + * + * @since 1.0 + */ +[nt:unstructured] + orderable + - * (UNDEFINED) multiple + - * (UNDEFINED) + + * (nt:base) = nt:unstructured sns VERSION + +//------------------------------------------------------------------------------ +// R E F E R E N C E A B L E +//------------------------------------------------------------------------------ + +/** + * This node type adds an auto-created, mandatory, protected STRING property to + * the node, called jcr:uuid, which exposes the identifier of the node. + * Note that the term "UUID" is used for backward compatibility with JCR 1.0 + * and does not necessarily imply the use of the UUID syntax, or global uniqueness. + * The identifier of a referenceable node must be a referenceable identifier. + * Referenceable identifiers must fulfill a number of constraints beyond the + * minimum required of standard identifiers (see 3.8.3 Referenceable Identifiers). + * A reference property is a property that holds the referenceable identifier of a + * referenceable node and therefore serves as a pointer to that node. The two types + * of reference properties, REFERENCE and WEAKREFERENCE differ in that the former + * enforces referential integrity while the latter does not. + * + * @since 1.0 + */ +[mix:referenceable] + mixin + - jcr:uuid (STRING) mandatory autocreated protected INITIALIZE + +//------------------------------------------------------------------------------ +// L O C K I N G +//------------------------------------------------------------------------------ + +/** + * @since 1.0 + */ +[mix:lockable] + mixin + - jcr:lockOwner (STRING) protected IGNORE + - jcr:lockIsDeep (BOOLEAN) protected IGNORE + +//------------------------------------------------------------------------------ +// S H A R E A B L E N O D E S +//------------------------------------------------------------------------------ + +/** + * @since 2.0 + */ +[mix:shareable] > mix:referenceable + mixin + +//------------------------------------------------------------------------------ +// V E R S I O N I N G +//------------------------------------------------------------------------------ + +/** + * @since 2.0 + */ +[mix:simpleVersionable] + mixin + - jcr:isCheckedOut (BOOLEAN) = 'true' mandatory autocreated protected IGNORE + +/** + * @since 1.0 + */ +[mix:versionable] > mix:simpleVersionable, mix:referenceable + mixin + - jcr:versionHistory (REFERENCE) mandatory protected IGNORE < 'nt:versionHistory' + - jcr:baseVersion (REFERENCE) mandatory protected IGNORE < 'nt:version' + - jcr:predecessors (REFERENCE) mandatory protected multiple IGNORE < 'nt:version' + - jcr:mergeFailed (REFERENCE) protected multiple ABORT < 'nt:version' + /** @since 2.0 */ + - jcr:activity (REFERENCE) protected < 'nt:activity' + /** @since 2.0 */ + - jcr:configuration (REFERENCE) protected IGNORE < 'nt:configuration' + +/** + * @since 1.0 + */ +[nt:versionHistory] > mix:referenceable + - jcr:versionableUuid (STRING) mandatory autocreated protected ABORT + /** @since 2.0 */ + - jcr:copiedFrom (WEAKREFERENCE) protected ABORT < 'nt:version' + + jcr:rootVersion (nt:version) = nt:version mandatory autocreated protected ABORT + + jcr:versionLabels (nt:versionLabels) = nt:versionLabels mandatory autocreated protected ABORT + + * (nt:version) = nt:version protected ABORT + +/** + * @since 1.0 + */ +[nt:versionLabels] + - * (REFERENCE) protected ABORT < 'nt:version' + +/** + * @since 1.0 + */ +[nt:version] > mix:referenceable + - jcr:created (DATE) mandatory autocreated protected ABORT + - jcr:predecessors (REFERENCE) protected multiple ABORT < 'nt:version' + - jcr:successors (REFERENCE) protected multiple ABORT < 'nt:version' + /** @since 2.0 */ + - jcr:activity (REFERENCE) protected ABORT < 'nt:activity' + + jcr:frozenNode (nt:frozenNode) protected ABORT + +/** + * @since 1.0 + */ +[nt:frozenNode] > mix:referenceable + orderable + - jcr:frozenPrimaryType (NAME) mandatory autocreated protected ABORT + - jcr:frozenMixinTypes (NAME) protected multiple ABORT + - jcr:frozenUuid (STRING) mandatory autocreated protected ABORT + - * (UNDEFINED) protected ABORT + - * (UNDEFINED) protected multiple ABORT + + * (nt:base) protected sns ABORT + +/** + * @since 1.0 + */ +[nt:versionedChild] + - jcr:childVersionHistory (REFERENCE) mandatory autocreated protected ABORT < 'nt:versionHistory' + +/** + * @since 2.0 + */ +[nt:activity] > mix:referenceable + - jcr:activityTitle (STRING) mandatory autocreated protected + +/** + * @since 2.0 + */ +[nt:configuration] > mix:versionable + - jcr:root (REFERENCE) mandatory autocreated protected + +//------------------------------------------------------------------------------ +// N O D E T Y P E S +//------------------------------------------------------------------------------ + +/** + * This node type is used to store a node type definition. Property and child node + * definitions within the node type definition are stored as same-name sibling nodes + * of type nt:propertyDefinition and nt:childNodeDefinition. + * + * @since 1.0 + */ +[nt:nodeType] + - jcr:nodeTypeName (NAME) protected mandatory + - jcr:supertypes (NAME) protected multiple + - jcr:isAbstract (BOOLEAN) protected mandatory + - jcr:isQueryable (BOOLEAN) protected mandatory + - jcr:isMixin (BOOLEAN) protected mandatory + - jcr:hasOrderableChildNodes (BOOLEAN) protected mandatory + - jcr:primaryItemName (NAME) protected + + jcr:propertyDefinition (nt:propertyDefinition) = nt:propertyDefinition protected sns + + jcr:childNodeDefinition (nt:childNodeDefinition) = nt:childNodeDefinition protected sns + +/** + * This node type used to store a property definition within a node type definition, + * which itself is stored as an nt:nodeType node. + * + * @since 1.0 + */ +[nt:propertyDefinition] + - jcr:name (NAME) protected + - jcr:autoCreated (BOOLEAN) protected mandatory + - jcr:mandatory (BOOLEAN) protected mandatory + - jcr:onParentVersion (STRING) protected mandatory + < 'COPY', 'VERSION', 'INITIALIZE', 'COMPUTE', 'IGNORE', 'ABORT' + - jcr:protected (BOOLEAN) protected mandatory + - jcr:requiredType (STRING) protected mandatory + < 'STRING', 'URI', 'BINARY', 'LONG', 'DOUBLE', + 'DECIMAL', 'BOOLEAN', 'DATE', 'NAME', 'PATH', + 'REFERENCE', 'WEAKREFERENCE', 'UNDEFINED' + - jcr:valueConstraints (STRING) protected multiple + - jcr:defaultValues (UNDEFINED) protected multiple + - jcr:multiple (BOOLEAN) protected mandatory + - jcr:availableQueryOperators (NAME) protected mandatory multiple + - jcr:isFullTextSearchable (BOOLEAN) protected mandatory + - jcr:isQueryOrderable (BOOLEAN) protected mandatory + +/** + * This node type used to store a child node definition within a node type definition, + * which itself is stored as an nt:nodeType node. + * + * @since 1.0 + */ +[nt:childNodeDefinition] + - jcr:name (NAME) protected + - jcr:autoCreated (BOOLEAN) protected mandatory + - jcr:mandatory (BOOLEAN) protected mandatory + - jcr:onParentVersion (STRING) protected mandatory + < 'COPY', 'VERSION', 'INITIALIZE', 'COMPUTE', 'IGNORE', 'ABORT' + - jcr:protected (BOOLEAN) protected mandatory + - jcr:requiredPrimaryTypes (NAME) = 'nt:base' protected mandatory multiple + - jcr:defaultPrimaryType (NAME) protected + - jcr:sameNameSiblings (BOOLEAN) protected mandatory + +//------------------------------------------------------------------------------ +// Q U E R Y +//------------------------------------------------------------------------------ + +/** + * @since 1.0 + */ +[nt:query] + - jcr:statement (STRING) + - jcr:language (STRING) + +/** + * Index definitions storage + * + * @since oak 0.6 + */ +[oak:queryIndexDefinition] > nt:unstructured + - type (STRING) + - reindex (BOOLEAN) mandatory IGNORE + +//------------------------------------------------------------------------------ +// L I F E C Y C L E M A N A G E M E N T +//------------------------------------------------------------------------------ + +/** + * Only nodes with mixin node type mix:lifecycle may participate in a lifecycle. + * + * @peop jcr:lifecyclePolicy + * This property is a reference to another node that contains + * lifecycle policy information. The definition of the referenced + * node is not specified. + * @prop jcr:currentLifecycleState + * This property is a string identifying the current lifecycle + * state of this node. The format of this string is not specified. + * + * @since 2.0 + */ +[mix:lifecycle] + mixin + - jcr:lifecyclePolicy (REFERENCE) protected INITIALIZE + - jcr:currentLifecycleState (STRING) protected INITIALIZE + +//------------------------------------------------------------------------------ +// J A C K R A B B I T I N T E R N A L S +//------------------------------------------------------------------------------ + +[rep:root] > nt:unstructured + + jcr:system (rep:system) = rep:system mandatory IGNORE + +[rep:system] + orderable + + jcr:versionStorage (rep:versionStorage) = rep:versionStorage mandatory protected ABORT + + jcr:nodeTypes (rep:nodeTypes) = rep:nodeTypes mandatory protected ABORT + // @since 2.0 + + jcr:activities (rep:Activities) = rep:Activities mandatory protected ABORT + // @since 2.0 + + jcr:configurations (rep:Configurations) = rep:Configurations protected ABORT + + * (nt:base) = nt:base IGNORE + // @since oak 1.0 + + rep:privileges (rep:Privileges) = rep:Privileges mandatory protected ABORT + + +/** + * Node Types (virtual) storage + */ +[rep:nodeTypes] + + * (nt:nodeType) = nt:nodeType protected ABORT + +/** + * Version storage + */ +[rep:versionStorage] + + * (nt:versionHistory) = nt:versionHistory protected ABORT + + * (rep:versionStorage) = rep:versionStorage protected ABORT + +/** + * Activities storage + * @since 2.0 + */ +[rep:Activities] + + * (nt:activity) = nt:activity protected ABORT + + * (rep:Activities) = rep:Activities protected ABORT + +/** + * the intermediate nodes for the configurations storage. + * Note: since the versionable node points to the configuration and vice versa, + * a configuration could never be removed because no such API exists. therefore + * the child node definitions are not protected. + * @since 2.0 + */ +[rep:Configurations] + + * (nt:configuration) = nt:configuration ABORT + + * (rep:Configurations) = rep:Configurations ABORT + +/** + * mixin that provides a multi value property for referencing versions. + * This is used for recording the baseline versions in the nt:configuration + * node, and to setup a bidirectional relationship between activities and + * the respective versions + * @since 2.0 + */ +[rep:VersionReference] mix + - rep:versions (REFERENCE) protected multiple + +// ----------------------------------------------------------------------------- +// J A C K R A B B I T S E C U R I T Y +// ----------------------------------------------------------------------------- + +[rep:AccessControllable] + mixin + + rep:policy (rep:Policy) protected IGNORE + +[rep:RepoAccessControllable] + mixin + + rep:repoPolicy (rep:Policy) protected IGNORE + +[rep:Policy] + abstract + +[rep:ACL] > rep:Policy + orderable + + * (rep:ACE) = rep:GrantACE protected IGNORE + +[rep:ACE] + - rep:principalName (STRING) protected mandatory + - rep:privileges (NAME) protected mandatory multiple + - rep:nodePath (PATH) protected + - rep:glob (STRING) protected + - * (UNDEFINED) protected + +[rep:GrantACE] > rep:ACE + +[rep:DenyACE] > rep:ACE + +// ----------------------------------------------------------------------------- +// Principal based AC +// ----------------------------------------------------------------------------- + +[rep:AccessControl] + + * (rep:AccessControl) protected IGNORE + + * (rep:PrincipalAccessControl) protected IGNORE + +[rep:PrincipalAccessControl] > rep:AccessControl + + rep:policy (rep:Policy) protected IGNORE + +// ----------------------------------------------------------------------------- +// User Management +// ----------------------------------------------------------------------------- + +[rep:Authorizable] > mix:referenceable, nt:hierarchyNode + abstract + + * (nt:base) = nt:unstructured VERSION + - rep:principalName (STRING) protected mandatory + - rep:authorizableId (STRING) protected + - * (UNDEFINED) + - * (UNDEFINED) multiple + +[rep:Impersonatable] + mixin + - rep:impersonators (STRING) protected multiple + +[rep:User] > rep:Authorizable, rep:Impersonatable + - rep:password (STRING) protected + - rep:disabled (STRING) protected + +[rep:Group] > rep:Authorizable + + rep:members (rep:Members) = rep:Members multiple protected VERSION + - rep:members (WEAKREFERENCE) protected multiple < 'rep:Authorizable' + +[rep:AuthorizableFolder] > nt:hierarchyNode + + * (rep:Authorizable) = rep:User VERSION + + * (rep:AuthorizableFolder) = rep:AuthorizableFolder VERSION + +[rep:Members] + orderable + + * (rep:Members) = rep:Members protected multiple + - * (WEAKREFERENCE) protected < 'rep:Authorizable' + +// ----------------------------------------------------------------------------- +// Privilege Management +// ----------------------------------------------------------------------------- + +/** + * @since oak 1.0 + */ +[rep:Privileges] + + * (rep:Privilege) = rep:Privilege protected ABORT + +/** + * @since oak 1.0 + */ +[rep:Privilege] + - rep:isAbstract (BOOLEAN) protected + - rep:aggregates (NAME) protected multiple + +// ----------------------------------------------------------------------------- +// Token Management +// ----------------------------------------------------------------------------- +/** + * @since oak 1.0 + */ +[rep:Token] > nt:unstructured, mix:referenceable + - rep:token.key (STRING) protected mandatory + - rep:token.exp (STRING) protected mandatory + +// ----------------------------------------------------------------------------- +// J A C K R A B B I T R E T E N T I O N M A N A G E M E N T +// ----------------------------------------------------------------------------- + +[rep:RetentionManageable] + mixin + - rep:hold (UNDEFINED) protected multiple IGNORE + - rep:retentionPolicy (UNDEFINED) protected IGNORE + +// ----------------------------------------------------------------------------- +// Oak save conflict resolution +// ----------------------------------------------------------------------------- + +[rep:MergeConflict] + mixin + primaryitem rep:ours + + rep:ours (nt:unstructured) protected IGNORE diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/HelloWorld.java b/oak-core/src/test/java/org/apache/jackrabbit/mk/HelloWorld.java deleted file mode 100644 index 489e3f39fc3..00000000000 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/HelloWorld.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk; - -import org.apache.jackrabbit.mk.api.MicroKernel; -import org.json.simple.parser.ParseException; - -/** - * A simple hello world app. - */ -public class HelloWorld { - - public static void main(String... args) throws ParseException { - test("fs:{homeDir};clean"); - test("simple:"); - test("simple:fs:target/temp;clean"); - } - - private static void test(String url) throws ParseException { - MicroKernel mk = MicroKernelFactory.getInstance(url); - System.out.println(url); - String head = mk.getHeadRevision(); - head = mk.commit("/", "+ \"hello\" : {}", head, null); - String move = "> \"hello\": \"world\" "; - String set = "^ \"world/x\": 1 "; - try { - head = mk.commit("/", move + set, head, null); - System.out.println("move & set worked"); - } catch (Exception e) { - System.out.println("move & set didn't work:"); - e.printStackTrace(System.out); - head = mk.commit("/", move, head, null); - head = mk.commit("/", set, head, null); - } - System.out.println(); - mk.dispose(); - } - -} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/api/MicroKernelTest.java b/oak-core/src/test/java/org/apache/jackrabbit/mk/api/MicroKernelTest.java deleted file mode 100644 index 62bd2e58a4d..00000000000 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/api/MicroKernelTest.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.api; - -import junit.framework.Assert; -import org.apache.jackrabbit.mk.MultiMkTestBase; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -@RunWith(Parameterized.class) -public class MicroKernelTest extends MultiMkTestBase { - - public MicroKernelTest(String url) { - super(url); - } - - @Before - public void setUp() throws Exception { - super.setUp(); - - String head = mk.getHeadRevision(); - mk.commit("/", "+\"test\" : {" + - "\"stringProp\":\"stringVal\"," + - "\"intProp\":42," + - "\"floatProp\":42.2," + - "\"multiIntProp\":[1,2,3]} ", head, ""); - } - - @Test - public void addAndMove() { - String head = mk.getHeadRevision(); - head = mk.commit("", - "+\"/root\":{}\n" + - "+\"/root/a\":{}\n" + - "", - head, ""); - - head = mk.commit("", - "+\"/root/a/b\":{}\n" + - ">\"/root/a\":\"/root/c\"\n" + - "", - head, ""); - - assertFalse(mk.nodeExists("/root/a", head)); - } - - - @Test - public void getNodes() { - String head = mk.getHeadRevision(); - - String json = mk.getNodes("/test", head, 0, 0, -1, null); - assertTrue(json.contains("stringProp")); - } - - @Test - public void missingName() { - String head = mk.getHeadRevision(); - - assertTrue(mk.nodeExists("/test", head)); - try { - String path = "/test/"; - mk.getNodes(path, head); - Assert.fail("Success with invalid path: " + path); - } catch (IllegalArgumentException e) { - // expected - } catch (MicroKernelException e) { - // expected - } - } - - @Test - public void addNodeWithRelativePath() { - String head = mk.getHeadRevision(); - - head = mk.commit("/", "+\"foo\" : {} \n+\"foo/bar\" : {}", head, ""); - assertTrue(mk.nodeExists("/foo", head)); - assertTrue(mk.nodeExists("/foo/bar", head)); - } - - @Test - public void commitWithEmptyPath() { - String head = mk.getHeadRevision(); - - head = mk.commit("", "+\"/ene\" : {}\n+\"/ene/mene\" : {}\n+\"/ene/mene/muh\" : {}", head, ""); - assertTrue(mk.nodeExists("/ene/mene/muh", head)); - } - - @Test - public void addPropertyWithRelativePath() { - String head = mk.getHeadRevision(); - - head = mk.commit("/", - "+\"fuu\" : {} \n" + - "^\"fuu/bar\" : 42", head, ""); - String n = mk.getNodes("/fuu", head); - assertEquals("{\"bar\":42,\":childNodeCount\":0}", n); - } - - @Test - public void addMultipleNodes() { - String head = mk.getHeadRevision(); - - long millis = System.currentTimeMillis(); - String node1 = "n1_" + millis; - String node2 = "n2_" + millis; - head = mk.commit("/", "+\"" + node1 + "\" : {} \n+\"" + node2 + "\" : {}\n", head, ""); - assertTrue(mk.nodeExists('/' + node1, head)); - assertTrue(mk.nodeExists('/' + node2, head)); - } - - @Test - public void addDeepNodes() { - String head = mk.getHeadRevision(); - - head = mk.commit("/", - "+\"a\" : {} \n" + - "+\"a/b\" : {} \n" + - "+\"a/b/c\" : {} \n" + - "+\"a/b/c/d\" : {} \n", - head, ""); - - assertTrue(mk.nodeExists("/a", head)); - assertTrue(mk.nodeExists("/a/b", head)); - assertTrue(mk.nodeExists("/a/b/c", head)); - assertTrue(mk.nodeExists("/a/b/c/d", head)); - } - - @Test - public void addItemsIncrementally() { - String head = mk.getHeadRevision(); - - String node = "n_" + System.currentTimeMillis(); - - head = mk.commit("/", - "+\"" + node + "\" : {} \n" + - "+\"" + node + "/child1\" : {} \n" + - "+\"" + node + "/child2\" : {} \n" + - "+\"" + node + "/child1/grandchild11\" : {} \n" + - "^\"" + node + "/prop1\" : 41\n" + - "^\"" + node + "/child1/prop2\" : 42\n" + - "^\"" + node + "/child1/grandchild11/prop3\" : 43", - head, ""); - - String json = mk.getNodes('/' + node, head, 3, 0, -1, null); - assertEquals("{\"prop1\":41,\":childNodeCount\":2," + - "\"child1\":{\"prop2\":42,\":childNodeCount\":1," + - "\"grandchild11\":{\"prop3\":43,\":childNodeCount\":0}}," + - "\"child2\":{\":childNodeCount\":0}}", json); - } - - @Test - public void removeNode() { - String head = mk.getHeadRevision(); - String node = "removeNode_" + System.currentTimeMillis(); - - head = mk.commit("/", "+\"" + node + "\" : {\"child\":{}}", head, ""); - - head = mk.commit('/' + node, "-\"child\"", head, ""); - String json = mk.getNodes('/' + node, head); - assertEquals("{\":childNodeCount\":0}", json); - } - - @Test - public void moveNode() { - String head = mk.getHeadRevision(); - String node = "moveNode_" + System.currentTimeMillis(); - String movedNode = "movedNode_" + System.currentTimeMillis(); - head = mk.commit("/", "+\"" + node + "\" : {}", head, ""); - - head = mk.commit("/", ">\"" + node + "\" : \"" + movedNode + '\"', head, ""); - assertFalse(mk.nodeExists('/' + node, head)); - assertTrue(mk.nodeExists('/' + movedNode, head)); - } - - @Test - public void overwritingMove() { - String head = mk.getHeadRevision(); - - head = mk.commit("/", "+\"a\" : {} \n+\"b\" : {} \n", head, ""); - try { - mk.commit("/", ">\"a\" : \"b\" ", head, ""); - fail(); - } catch (MicroKernelException e) { - // expected - } - } - - @Test - public void conflictingMove() { - String head = mk.getHeadRevision(); - - head = mk.commit("/", "+\"a\" : {} \n+\"b\" : {}\n", head, ""); - - String r1 = mk.commit("/", ">\"a\" : \"b/a\"", head, ""); - assertFalse(mk.nodeExists("/a", r1)); - assertTrue(mk.nodeExists("/b", r1)); - assertTrue(mk.nodeExists("/b/a", r1)); - - try { - mk.commit("/", ">\"b\" : \"a/b\"", head, ""); - fail(); - } catch (MicroKernelException e) { - // expected - } - } - - @Test - public void conflictingAddDelete() { - String head = mk.getHeadRevision(); - - head = mk.commit("/", "+\"a\" : {} \n+\"b\" : {}\n", head, ""); - - String r1 = mk.commit("/", "-\"b\" \n +\"a/x\" : {}", head, ""); - assertFalse(mk.nodeExists("/b", r1)); - assertTrue(mk.nodeExists("/a", r1)); - assertTrue(mk.nodeExists("/a/x", r1)); - - try { - mk.commit("/", "-\"a\" \n +\"b/x\" : {}", head, ""); - fail(); - } catch (MicroKernelException e) { - // expected - } - } - - @Test - public void reorderNode() { - if (!isSimpleKernel(mk)) { - return; - } - String head = mk.getHeadRevision(); - String node = "reorderNode_" + System.currentTimeMillis(); - head = mk.commit("/", "+\"" + node + "\" : {\"a\":{}, \"b\":{}, \"c\":{}}", head, ""); - // System.out.println(mk.getNodes('/' + node, head).replaceAll("\"", "").replaceAll(":childNodeCount:.", "")); - - head = mk.commit("/", ">\"" + node + "/a\" : {\"before\":\"" + node + "/c\"}", head, ""); - // System.out.println(mk.getNodes('/' + node, head).replaceAll("\"", "").replaceAll(":childNodeCount:.", "")); - } - - @Test - public void removeProperty() { - String head = mk.getHeadRevision(); - long t = System.currentTimeMillis(); - String node = "removeProperty_" + t; - - head = mk.commit("/", "+\"" + node + "\" : {\"prop\":\"value\"}", head, ""); - - head = mk.commit("/", "^\"" + node + "/prop\" : null", head, ""); - String json = mk.getNodes('/' + node, head); - assertEquals("{\":childNodeCount\":0}", json); - } -} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/cluster/BasicTest.java b/oak-core/src/test/java/org/apache/jackrabbit/mk/cluster/BasicTest.java deleted file mode 100644 index 8e908e5011d..00000000000 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/cluster/BasicTest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.cluster; - -import org.apache.jackrabbit.mk.MicroKernelFactory; -import org.apache.jackrabbit.mk.api.MicroKernel; -import org.apache.jackrabbit.mk.server.Server; -import org.junit.After; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; - -public class BasicTest { - - private MicroKernel mk1; - private Server server; - - private MicroKernel mk2; - - @Before - public void setUp() throws Exception { - mk1 = MicroKernelFactory.getInstance("fs:{homeDir}/target/mk1;clean"); - mk2 = MicroKernelFactory.getInstance("fs:{homeDir}/target/mk2;clean"); - } - - @After - public void tearDown() { - if (mk1 != null) { - mk1.dispose(); - } - if (mk2 != null) { - mk2.dispose(); - } - if (server != null) { - server.stop(); - } - } - - @Test - @Ignore - public void test() throws Exception { - server = new Server(mk1); - server.setPort(0); - server.start(); - - ClusterNode cn = new ClusterNode(mk2); - cn.join(server.getAddress()); - - mk1.commit("/", "+\"test\":{}", mk1.getHeadRevision(), null); - mk2.getNodes("/test", mk2.getHeadRevision()); - } -} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/cluster/HotBackupTest.java b/oak-core/src/test/java/org/apache/jackrabbit/mk/cluster/HotBackupTest.java deleted file mode 100644 index 2e058d7647a..00000000000 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/cluster/HotBackupTest.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.cluster; - -import org.apache.jackrabbit.mk.MicroKernelFactory; -import org.apache.jackrabbit.mk.api.MicroKernel; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -public class HotBackupTest { - - private MicroKernel source; - private MicroKernel target; - - @Before - public void setUp() throws Exception { - source = MicroKernelFactory.getInstance("fs:{homeDir}/target/mk1;clean"); - target = MicroKernelFactory.getInstance("fs:{homeDir}/target/mk2;clean"); - } - - @After - public void tearDown() { - if (source != null) { - source.dispose(); - } - if (target != null) { - target.dispose(); - } - } - - @Test - public void test() { - HotBackup hotbackup = new HotBackup(source, target); - source.commit("/", "+\"test\":{}", source.getHeadRevision(), null); - hotbackup.sync(); - target.getNodes("/test", target.getHeadRevision()); - } -} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/fs/FileSystemTest.java b/oak-core/src/test/java/org/apache/jackrabbit/mk/fs/FileSystemTest.java deleted file mode 100644 index a2a4e0f154b..00000000000 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/fs/FileSystemTest.java +++ /dev/null @@ -1,483 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.fs; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.RandomAccessFile; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; -import java.nio.channels.FileChannel.MapMode; -import java.util.List; -import java.util.Random; -import junit.framework.TestCase; -import org.apache.jackrabbit.mk.util.IOUtilsTest; - -/** - * Tests various file system. - */ -public class FileSystemTest extends TestCase { - - private String getBaseDir() { - return "target/temp"; - } - - public void test() throws Exception { - testFileSystem(getBaseDir() + "/fs"); - testFileSystem("cache:" + getBaseDir() + "/fs"); - } - - public void testAbsoluteRelative() { - assertTrue(FileUtils.isAbsolute("/test/abc")); - assertFalse(FileUtils.isAbsolute("test/abc")); - assertTrue(FileUtils.isAbsolute("~/test/abc")); - } - - public void testClasspath() throws IOException { - String resource = getClass().getName().replace('.', '/') + ".class"; - InputStream in; - in = getClass().getResourceAsStream("/" + resource); - assertTrue(in != null); - in.close(); - in = getClass().getClassLoader().getResourceAsStream(resource); - assertTrue(in != null); - in.close(); - in = FileUtils.newInputStream("classpath:" + resource); - assertTrue(in != null); - in.close(); - in = FileUtils.newInputStream("classpath:/" + resource); - assertTrue(in != null); - in.close(); - } - - public void testSimpleExpandTruncateSize() throws Exception { - String f = getBaseDir() + "/fs/test.data"; - FileUtils.createDirectories(getBaseDir() + "/fs"); - FileChannel c = FileUtils.open(f, "rw"); - c.position(4000); - c.write(ByteBuffer.wrap(new byte[1])); - FileLock lock = c.tryLock(); - c.truncate(0); - if (lock != null) { - lock.release(); - } - c.close(); - } - - public void testUserHome() throws IOException { - String userDir = System.getProperty("user.home").replace('\\', '/'); - assertTrue(FileUtils.toRealPath("~/test").startsWith(userDir)); - assertTrue(FileUtils.toRealPath("file:~/test").startsWith(userDir)); - } - - private void testFileSystem(String fsBase) throws Exception { - testAppend(fsBase); - testDirectories(fsBase); - testMoveTo(fsBase); - testParentEventuallyReturnsNull(fsBase); - testRandomAccess(fsBase); - testResolve(fsBase); - testSetReadOnly(fsBase); - testSimple(fsBase); - testTempFile(fsBase); - testUnsupportedFeatures(fsBase); - } - - public void testAppend(String fsBase) throws IOException { - String fileName = fsBase + "/testFile.txt"; - if (FileUtils.exists(fileName)) { - FileUtils.delete(fileName); - } - FileUtils.createDirectories(FileUtils.getParent(fileName)); - FileUtils.createFile(fileName); - // Profiler prof = new Profiler(); - // prof.interval = 1; - // prof.startCollecting(); - FileChannel c = FileUtils.open(fileName, "rw"); - c.position(0); - // long t = System.currentTimeMillis(); - byte[] array = new byte[100]; - ByteBuffer buff = ByteBuffer.wrap(array); - for (int i = 0; i < 100000; i++) { - array[0] = (byte) i; - c.write(buff); - buff.rewind(); - } - c.close(); - // System.out.println(fsBase + ": " + (System.currentTimeMillis() - t)); - // System.out.println(prof.getTop(10)); - FileUtils.delete(fileName); - } - - private void testDirectories(String fsBase) throws IOException { - final String fileName = fsBase + "/testFile"; - if (FileUtils.exists(fileName)) { - FileUtils.delete(fileName); - } - if (FileUtils.createFile(fileName)) { - try { - FileUtils.createDirectory(fileName); - fail(); - } catch (IOException e) { - // expected - } - try { - FileUtils.createDirectories(fileName + "/test"); - fail(); - } catch (IOException e) { - // expected - } - FileUtils.delete(fileName); - } - } - - private void testMoveTo(String fsBase) throws IOException { - final String fileName = fsBase + "/testFile"; - final String fileName2 = fsBase + "/testFile2"; - if (FileUtils.exists(fileName)) { - FileUtils.delete(fileName); - } - if (FileUtils.createFile(fileName)) { - FileUtils.moveTo(fileName, fileName2); - FileUtils.createFile(fileName); - try { - FileUtils.moveTo(fileName2, fileName); - fail(); - } catch (IOException e) { - // expected - } - FileUtils.delete(fileName); - FileUtils.delete(fileName2); - try { - FileUtils.moveTo(fileName, fileName2); - fail(); - } catch (IOException e) { - // expected - } - } - } - - private void testParentEventuallyReturnsNull(String fsBase) { - FilePath p = FilePath.get(fsBase + "/testFile"); - assertTrue(p.getScheme().length() > 0); - for (int i = 0; i < 100; i++) { - if (p == null) { - return; - } - p = p.getParent(); - } - fail("Parent is not null: " + p); - String path = fsBase + "/testFile"; - for (int i = 0; i < 100; i++) { - if (path == null) { - return; - } - path = FileUtils.getParent(path); - } - fail("Parent is not null: " + path); - } - - private void testResolve(String fsBase) { - String fileName = fsBase + "/testFile"; - assertEquals(fileName, FilePath.get(fsBase).resolve("testFile").toString()); - } - - private void testSetReadOnly(String fsBase) throws IOException { - String fileName = fsBase + "/testFile"; - if (FileUtils.exists(fileName)) { - FileUtils.delete(fileName); - } - if (FileUtils.createFile(fileName)) { - FileUtils.setReadOnly(fileName); - assertFalse(FileUtils.canWrite(fileName)); - FileUtils.delete(fileName); - } - } - - private void testSimple(final String fsBase) throws Exception { - long time = System.currentTimeMillis(); - for (String s : FileUtils.newDirectoryStream(fsBase)) { - FileUtils.delete(s); - } - FileUtils.createDirectories(fsBase + "/test"); - FileUtils.delete(fsBase + "/test"); - FileUtils.delete(fsBase + "/test2"); - assertTrue(FileUtils.createFile(fsBase + "/test")); - List p = FilePath.get(fsBase).newDirectoryStream(); - assertEquals(1, p.size()); - String can = FilePath.get(fsBase + "/test").toRealPath().toString(); - assertEquals(can, p.get(0).toString()); - assertTrue(FileUtils.canWrite(fsBase + "/test")); - FileChannel channel = FileUtils.open(fsBase + "/test", "rw"); - byte[] buffer = new byte[10000]; - Random random = new Random(1); - random.nextBytes(buffer); - channel.write(ByteBuffer.wrap(buffer)); - assertEquals(10000, channel.size()); - channel.position(20000); - assertEquals(20000, channel.position()); - assertEquals(-1, channel.read(ByteBuffer.wrap(buffer, 0, 1))); - String path = fsBase + "/test"; - assertEquals("test", FileUtils.getName(path)); - can = FilePath.get(fsBase).toRealPath().toString(); - String can2 = FileUtils.toRealPath(FileUtils.getParent(path)); - assertEquals(can, can2); - FileLock lock = channel.tryLock(); - if (lock != null) { - lock.release(); - } - assertEquals(10000, channel.size()); - channel.close(); - assertEquals(10000, FileUtils.size(fsBase + "/test")); - channel = FileUtils.open(fsBase + "/test", "r"); - final byte[] test = new byte[10000]; - FileUtils.readFully(channel, ByteBuffer.wrap(test, 0, 10000)); - IOUtilsTest.assertEquals(buffer, test); - final FileChannel fc = channel; - try { - fc.write(ByteBuffer.wrap(test, 0, 10)); - fail(); - } catch (IOException e) { - // expected - } - try { - fc.truncate(10); - fail(); - } catch (IOException e) { - // expected - } - channel.close(); - long lastMod = FileUtils.lastModified(fsBase + "/test"); - if (lastMod < time - 1999) { - // at most 2 seconds difference - assertEquals(time, lastMod); - } - assertEquals(10000, FileUtils.size(fsBase + "/test")); - List list = FileUtils.newDirectoryStream(fsBase); - assertEquals(1, list.size()); - assertTrue(list.get(0).endsWith("test")); - FileUtils.copy(fsBase + "/test", fsBase + "/test3"); - FileUtils.moveTo(fsBase + "/test3", fsBase + "/test2"); - assertTrue(!FileUtils.exists(fsBase + "/test3")); - assertTrue(FileUtils.exists(fsBase + "/test2")); - assertEquals(10000, FileUtils.size(fsBase + "/test2")); - byte[] buffer2 = new byte[10000]; - InputStream in = FileUtils.newInputStream(fsBase + "/test2"); - int pos = 0; - while (true) { - int l = in.read(buffer2, pos, Math.min(10000 - pos, 1000)); - if (l <= 0) { - break; - } - pos += l; - } - in.close(); - assertEquals(10000, pos); - IOUtilsTest.assertEquals(buffer, buffer2); - - assertTrue(FileUtils.tryDelete(fsBase + "/test2")); - FileUtils.delete(fsBase + "/test"); - FileUtils.createDirectories(fsBase + "/testDir"); - assertTrue(FileUtils.isDirectory(fsBase + "/testDir")); - if (!fsBase.startsWith("jdbc:")) { - FileUtils.deleteRecursive(fsBase + "/testDir", false); - assertTrue(!FileUtils.exists(fsBase + "/testDir")); - } - } - - private void testRandomAccess(String fsBase) throws Exception { - testRandomAccess(fsBase, 1); - } - - private void testRandomAccess(String fsBase, int seed) throws Exception { - StringBuilder buff = new StringBuilder(); - String s = FileUtils.createTempFile(fsBase + "/tmp", ".tmp", false, false); - File file = new File(getBaseDir() + "/tmp"); - file.getParentFile().mkdirs(); - file.delete(); - RandomAccessFile ra = new RandomAccessFile(file, "rw"); - FileUtils.delete(s); - FileChannel f = FileUtils.open(s, "rw"); - assertEquals(-1, f.read(ByteBuffer.wrap(new byte[1]))); - f.force(true); - Random random = new Random(seed); - int size = 500; - try { - for (int i = 0; i < size; i++) { - trace("op " + i); - int pos = random.nextInt(10000); - switch(random.nextInt(7)) { - case 0: { - pos = (int) Math.min(pos, ra.length()); - trace("seek " + pos); - buff.append("seek " + pos + "\n"); - f.position(pos); - ra.seek(pos); - break; - } - case 1: { - byte[] buffer = new byte[random.nextInt(1000)]; - random.nextBytes(buffer); - trace("write " + buffer.length); - buff.append("write " + buffer.length + "\n"); - f.write(ByteBuffer.wrap(buffer)); - ra.write(buffer, 0, buffer.length); - break; - } - case 2: { - trace("truncate " + pos); - f.truncate(pos); - if (pos < ra.length()) { - // truncate is supposed to have no effect if the - // position is larger than the current size - ra.setLength(pos); - } - assertEquals("truncate " + pos, ra.getFilePointer(), f.position()); - buff.append("truncate " + pos + "\n"); - break; - } - case 3: { - int len = random.nextInt(1000); - len = (int) Math.min(len, ra.length() - ra.getFilePointer()); - byte[] b1 = new byte[len]; - byte[] b2 = new byte[len]; - trace("readFully " + len); - ra.readFully(b1, 0, len); - FileUtils.readFully(f, ByteBuffer.wrap(b2, 0, len)); - buff.append("readFully " + len + "\n"); - IOUtilsTest.assertEquals(b1, b2); - break; - } - case 4: { - trace("getFilePointer " + ra.getFilePointer()); - buff.append("getFilePointer " + ra.getFilePointer() + "\n"); - assertEquals(ra.getFilePointer(), f.position()); - break; - } - case 5: { - trace("length " + ra.length()); - buff.append("length " + ra.length() + "\n"); - assertEquals(ra.length(), f.size()); - break; - } - case 6: { - trace("reopen"); - buff.append("reopen\n"); - f.close(); - ra.close(); - ra = new RandomAccessFile(file, "rw"); - f = FileUtils.open(s, "rw"); - assertEquals(ra.length(), f.size()); - break; - } - default: - } - } - } catch (Throwable e) { - e.printStackTrace(); - fail("Exception: " + e + "\n"+ buff.toString()); - } finally { - f.close(); - ra.close(); - file.delete(); - FileUtils.delete(s); - } - } - - private void testTempFile(String fsBase) throws Exception { - int len = 10000; - String s = FileUtils.createTempFile(fsBase + "/tmp", ".tmp", false, false); - OutputStream out = FileUtils.newOutputStream(s, false); - byte[] buffer = new byte[len]; - out.write(buffer); - out.close(); - out = FileUtils.newOutputStream(s, true); - out.write(1); - out.close(); - InputStream in = FileUtils.newInputStream(s); - for (int i = 0; i < len; i++) { - assertEquals(0, in.read()); - } - assertEquals(1, in.read()); - assertEquals(-1, in.read()); - in.close(); - out.close(); - FileUtils.delete(s); - } - - private void testUnsupportedFeatures(String fsBase) throws IOException { - final String fileName = fsBase + "/testFile"; - if (FileUtils.exists(fileName)) { - FileUtils.delete(fileName); - } - if (FileUtils.createFile(fileName)) { - final FileChannel channel = FileUtils.open(fileName, "rw"); - try { - channel.map(MapMode.PRIVATE, 0, channel.size()); - fail(); - } catch (UnsupportedOperationException e) { - // expected - } - try { - channel.read(ByteBuffer.allocate(10), 0); - fail(); - } catch (UnsupportedOperationException e) { - // expected - } - try { - channel.read(new ByteBuffer[]{ByteBuffer.allocate(10)}, 0, 0); - fail(); - } catch (UnsupportedOperationException e) { - // expected - } - try { - channel.write(ByteBuffer.allocate(10), 0); - fail(); - } catch (UnsupportedOperationException e) { - // expected - } - try { - channel.write(new ByteBuffer[]{ByteBuffer.allocate(10)}, 0, 0); - fail(); - } catch (UnsupportedOperationException e) { - // expected - } - try { - channel.transferFrom(channel, 0, 0); - fail(); - } catch (UnsupportedOperationException e) { - // expected - } - try { - channel.transferTo(0, 0, channel); - fail(); - } catch (UnsupportedOperationException e) { - // expected - } - channel.close(); - FileUtils.delete(fileName); - } - } - - private void trace(String s) { - // System.out.println(s); - } - -} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/json/JsonBuilderTest.java b/oak-core/src/test/java/org/apache/jackrabbit/mk/json/JsonBuilderTest.java deleted file mode 100644 index f5e52a80085..00000000000 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/json/JsonBuilderTest.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.jackrabbit.mk.json; - -import junit.framework.Assert; -import org.apache.jackrabbit.mk.json.JsonBuilder.JsonArrayBuilder; -import org.apache.jackrabbit.mk.json.JsonBuilder.JsonObjectBuilder; -import org.json.simple.parser.ContentHandler; -import org.json.simple.parser.JSONParser; -import org.json.simple.parser.ParseException; -import org.junit.Test; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.StringReader; -import java.io.StringWriter; -import java.math.BigDecimal; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; - -public class JsonBuilderTest { - - @Test - public void jsonBuilder() throws IOException { - - StringWriter sw = new StringWriter(); - JsonBuilder.create(sw) - .value("foo", "bar") - .value("int", 3) - .value("float", 3f) - .object("obj") - .value("boolean", true) - .nil("nil") - .array("arr") - .value(1) - .value(2.0f) - .value(2.0d) - .value("42") - .build() - .build() - .array("string array", new String[]{"", "1", "foo"}) - .array("int array", new int[]{1, 2, 3}) - .array("long array", new long[]{1, 2, 3}) - .array("float array", new float[]{1, 2, 3}) - .array("double array", new double[]{1, 2, 3}) - .array("boolean array", new boolean[]{true, false}) - .array("number array", new BigDecimal[]{new BigDecimal(21), new BigDecimal(42)}) - .value("some", "more") - .build(); - - String json = sw.toString(); - assertEquals("{\"foo\":\"bar\",\"int\":3,\"float\":3.0,\"obj\":{\"boolean\":true,\"nil\":null," + - "\"arr\":[1,2.0,2.0,\"42\"]},\"string array\":[\"\",\"1\",\"foo\"],\"int array\":[1,2,3]," + - "\"long array\":[1,2,3],\"float array\":[1.0,2.0,3.0],\"double array\":[1.0,2.0,3.0]," + - "\"boolean array\":[true,false],\"number array\":[21,42],\"some\":\"more\"}", json); - } - - @Test - public void escape() throws IOException { - StringWriter sw = new StringWriter(); - JsonBuilder.create(sw) - .value("back\\slash", "\\") - .value("back\\\\slash", "\\\\") - .build(); - - String json = sw.toString(); - assertEquals("{\"back\\\\slash\":\"\\\\\",\"back\\\\\\\\slash\":\"\\\\\\\\\"}", json); - } - - @Test - public void fixedPoint() throws IOException, ParseException { - InputStream one = JsonBuilderTest.class.getResourceAsStream("test.json"); - assertNotNull(one); - InputStreamReader isr = new InputStreamReader(one); - - String s1 = fix(isr); - String s2 = fix(s1); - - // fix == fix fix - assertEquals(s1, s2); - } - - //------------------------------------------< private >--- - - private static String fix(Reader reader) throws IOException, ParseException { - StringWriter sw = new StringWriter(); - new JSONParser().parse(reader, new JsonHandler(JsonBuilder.create(sw))); - return sw.toString(); - } - - private static String fix(String string) throws IOException, ParseException { - return fix(new StringReader(string)); - } - - private static class JsonHandler implements ContentHandler { - private JsonObjectBuilder objectBuilder; - private JsonArrayBuilder arrayBuilder; - private String currentKey; - - public JsonHandler(JsonObjectBuilder objectBuilder) { - this.objectBuilder = objectBuilder; - } - - public void startJSON() throws ParseException, IOException { - // ignore - } - - public void endJSON() throws ParseException, IOException { - // ignore - } - - public boolean startObject() throws ParseException, IOException { - if (currentKey != null) { - objectBuilder = objectBuilder.object(currentKey); - } - return true; - } - - public boolean endObject() throws ParseException, IOException { - objectBuilder = objectBuilder.build(); - return true; - } - - public boolean startObjectEntry(String key) throws ParseException, IOException { - currentKey = key; - return true; - } - - public boolean endObjectEntry() throws ParseException, IOException { - return true; - } - - public boolean startArray() throws ParseException, IOException { - arrayBuilder = objectBuilder.array(currentKey); - return true; - } - - public boolean endArray() throws ParseException, IOException { - objectBuilder = arrayBuilder.build(); - arrayBuilder = null; - return true; - } - - public boolean primitive(Object value) throws ParseException, IOException { - if (arrayBuilder == null) { - if (value == null) { - objectBuilder.nil(currentKey); - } else if (value instanceof String) { - objectBuilder.value(currentKey, (String) value); - } else if (value instanceof Integer) { - objectBuilder.value(currentKey, ((Integer) value).intValue()); - } else if (value instanceof Long) { - objectBuilder.value(currentKey, ((Long) value).longValue()); - } else if (value instanceof Double) { - objectBuilder.value(currentKey, ((Double) value).doubleValue()); - } else if (value instanceof Float) { - objectBuilder.value(currentKey, ((Float) value).floatValue()); - } else if (value instanceof Boolean) { - objectBuilder.value(currentKey, (Boolean) value); - } else { - Assert.fail(); - } - } else { - if (value == null) { - arrayBuilder.nil(); - } else if (value instanceof String) { - arrayBuilder.value((String) value); - } else if (value instanceof Integer) { - arrayBuilder.value(((Integer) value).intValue()); - } else if (value instanceof Long) { - arrayBuilder.value(((Long) value).longValue()); - } else if (value instanceof Double) { - arrayBuilder.value(((Double) value).doubleValue()); - } else if (value instanceof Float) { - arrayBuilder.value(((Float) value).floatValue()); - } else if (value instanceof Boolean) { - arrayBuilder.value((Boolean) value); - } else { - Assert.fail(); - } - } - return true; - } - } -} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/store/CopyHeadRevisionTest.java b/oak-core/src/test/java/org/apache/jackrabbit/mk/store/CopyHeadRevisionTest.java deleted file mode 100644 index e4f59041b98..00000000000 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/store/CopyHeadRevisionTest.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.store; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -import java.io.File; - -import org.apache.jackrabbit.mk.MicroKernelImpl; -import org.apache.jackrabbit.mk.Repository; -import org.apache.jackrabbit.mk.api.MicroKernel; -import org.apache.jackrabbit.mk.api.MicroKernelException; -import org.apache.jackrabbit.mk.fs.FileUtils; -import org.apache.jackrabbit.mk.json.fast.Jsop; -import org.apache.jackrabbit.mk.json.fast.JsopArray; -import org.apache.jackrabbit.mk.json.fast.JsopObject; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -/** - * Use-case: start off a new revision store that contains just the head revision - * and its nodes. - * - * TODO: make the test concurrent - */ -public class CopyHeadRevisionTest { - - @Before - public void setup() throws Exception { - FileUtils.deleteRecursive("target/mk1", false); - FileUtils.deleteRecursive("target/mk2", false); - } - - @After - public void tearDown() throws Exception { - } - - @Test - public void testCopyHeadRevisionToNewStore() throws Exception { - String[] revs = new String[5]; - - DefaultRevisionStore rsFrom = new DefaultRevisionStore(); - rsFrom.initialize(new File("target/mk1")); - - DefaultRevisionStore rsTo = new DefaultRevisionStore(); - rsTo.initialize(new File("target/mk2")); - - CopyingGC gc = new CopyingGC(rsFrom, rsTo); - - MicroKernel mk = new MicroKernelImpl(new Repository(gc)); - revs[0] = mk.commit("/", "+\"a\" : { \"c\":{}, \"d\":{} }", mk.getHeadRevision(), null); - revs[1] = mk.commit("/", "+\"b\" : {}", mk.getHeadRevision(), null); - revs[2] = mk.commit("/b", "+\"e\" : {}", mk.getHeadRevision(), null); - revs[3] = mk.commit("/a/c", "+\"f\" : {}", mk.getHeadRevision(), null); - - // Simulate a GC cycle start - gc.start(); - - revs[4] = mk.commit("/b/e", "+\"g\" : {}", mk.getHeadRevision(), null); - mk.getJournal(revs[2], revs[2], ""); - - // Simulate a GC cycle stop - gc.stop(); - - // Assert head revision is contained after GC - assertEquals(mk.getHeadRevision(), revs[revs.length - 1]); - - // Assert unused revision was GCed - try { - mk.getNodes("/", revs[0]); - fail("Revision should have been GCed: "+ revs[0]); - } catch (MicroKernelException e) { - // ignore - } - - // Verify journal integrity: referenced revision must still be available and linked in chain - JsopArray a = (JsopArray) Jsop.parse(mk.getRevisions(0, Integer.MAX_VALUE)); - for (int i = 0; i < a.size(); i++) { - if (((JsopObject) a.get(i)).get("id").equals(revs[2])) { - return; - } - } - fail("Revision not appearing in list of revisions: "+ revs[2]); - } -} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/BloomFilterUtilsTest.java b/oak-core/src/test/java/org/apache/jackrabbit/mk/util/BloomFilterUtilsTest.java deleted file mode 100644 index 1d122cd5639..00000000000 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/BloomFilterUtilsTest.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.util; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import java.math.BigInteger; -import java.util.HashSet; -import java.util.Random; -import org.junit.Test; - -/** - * Test the bloom filter utility class. - */ -public class BloomFilterUtilsTest { - - /** - * Program to calculate the best shift and multiply constants. - */ - public static void main(String... args) { - Random random = new Random(1); - HashSet inSet = new HashSet(); - while (inSet.size() < 100) { - inSet.add(randomString(random)); - } - Object[] in = inSet.toArray(); - HashSet notSet = new HashSet(); - while (notSet.size() < 10000) { - String k = randomString(random); - if (!inSet.contains(k)) { - notSet.add(k); - } - } - Object[] not = notSet.toArray(); - int best = Integer.MAX_VALUE; - for (int mul = 1; mul < 100000; mul += 2) { - if (!BigInteger.valueOf(mul).isProbablePrime(10)) { - continue; - } - for (int shift = 0; shift < 32; shift++) { - byte[] bloom = BloomFilterUtils.createFilter(100, 64); - for (Object k : in) { - int h1 = hash(k.hashCode(), mul, shift), h2 = hash(h1, mul, shift); - add(bloom, h1, h2); - } - int falsePositives = 0; - for (Object k : not) { - int h1 = hash(k.hashCode(), mul, shift), h2 = hash(h1, mul, shift); - if (probablyContains(bloom, h1, h2)) { - falsePositives++; - // short false positives are bad - if (k.toString().length() < 4) { - falsePositives += 5; - } - if (falsePositives > best) { - break; - } - } - } - if (falsePositives < best) { - best = falsePositives; - System.out.println("mul: " + mul + " shift: " - + shift + " falsePositives: " + best); - } - } - } - } - - private static String randomString(Random r) { - if (r.nextInt(5) == 0) { - return randomName(r); - } - int length = 1 + Math.abs((int) r.nextGaussian() * 5); - if (r.nextBoolean()) { - length += r.nextInt(10); - } - char[] chars = new char[length]; - for (int i = 0; i < length; i++) { - chars[i] = randomChar(r); - } - return new String(chars); - } - - private static char randomChar(Random r) { - switch (r.nextInt(101) / 100) { - case 0: - case 1: - // 20% ascii - return (char) (32 + r.nextInt(127 - 32)); - case 2: - case 3: - case 4: - case 5: - // 40% a-z - return (char) ('a' + r.nextInt('z' - 'a')); - case 6: - // 10% A-Z - return (char) ('A' + r.nextInt('Z' - 'A')); - case 7: - case 8: - // 20% 0-9 - return (char) ('0' + r.nextInt('9' - '0')); - case 9: - // 10% aeiou - return "aeiou".charAt(r.nextInt("aeiou".length())); - } - // 1% unicode - return (char) r.nextInt(65535); - } - - private static String randomName(Random r) { - int i = r.nextInt(1000); - // like TPC-C lastName, but lowercase - String[] n = { - "bar", "ought", "able", "pri", "pres", "ese", "anti", - "cally", "ation", "eing" }; - StringBuilder buff = new StringBuilder(); - buff.append(n[i / 100]); - buff.append(n[(i / 10) % 10]); - buff.append(n[i % 10]); - return buff.toString(); - } - - - private static int hash(int oldHash, int mul, int shift) { - return oldHash ^ ((oldHash * mul) >> shift); - } - - private static void add(byte[] bloom, int h1, int h2) { - int len = bloom.length; - if (len > 0) { - bloom[(h1 >>> 3) % len] |= 1 << (h1 & 7); - bloom[(h2 >>> 3) % len] |= 1 << (h2 & 7); - } - } - - private static boolean probablyContains(byte[] bloom, int h1, int h2) { - int len = bloom.length; - if (len == 0) { - return true; - } - int x = bloom[(h1 >>> 3) % len] & (1 << (h1 & 7)); - if (x != 0) { - x = bloom[(h2 >>> 3) % len] & (1 << (h2 & 7)); - } - return x != 0; - } - - @Test - public void size() { - byte[] bloom = BloomFilterUtils.createFilter(100, 64); - assertEquals(64, bloom.length); - bloom = BloomFilterUtils.createFilter(10, 64); - assertEquals(11, bloom.length); - bloom = BloomFilterUtils.createFilter(0, 64); - assertEquals(0, bloom.length); - bloom = BloomFilterUtils.createFilter(1, 64); - assertEquals(1, bloom.length); - } - - @Test - public void probability() { - byte[] bloom = BloomFilterUtils.createFilter(20, 64); - for (int i = 0; i < 20; i++) { - BloomFilterUtils.add(bloom, String.valueOf(i)); - } - for (int i = 0; i < 20; i++) { - assertTrue(BloomFilterUtils.probablyContains(bloom, String.valueOf(i))); - } - int falsePositives = 0; - for (int i = 20; i < 100000; i++) { - if (BloomFilterUtils.probablyContains(bloom, String.valueOf(i))) { - falsePositives++; - } - } - assertEquals(1101, falsePositives); - } - @Test - public void negativeHashCode() { - BloomFilterUtils.add(new byte[0], new Object() { - public int hashCode() { - return -1; - } - }); - } - -} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/SyncTest.java b/oak-core/src/test/java/org/apache/jackrabbit/mk/util/SyncTest.java deleted file mode 100644 index 8668332766b..00000000000 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/SyncTest.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law - * or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ -package org.apache.jackrabbit.mk.util; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import java.util.HashSet; -import java.util.Iterator; -import org.apache.jackrabbit.mk.MultiMkTestBase; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -/** - * Test the sync util. - */ -@RunWith(Parameterized.class) -public class SyncTest extends MultiMkTestBase { - - public SyncTest(String url) { - super(url); - } - - @Test - public void testIterator() { - HashSet names = new HashSet(); - for (int i = 0; i < 20; i++) { - mk.commit("/", "+ \"n" + i + "\": {}", mk.getHeadRevision(), ""); - names.add("n" + i); - } - String head = mk.getHeadRevision(); - Iterator it = Sync.getAllChildNodeNames(mk, "/", head, 2); - while (it.hasNext()) { - String n = it.next(); - assertTrue(names.remove(n)); - } - assertEquals(0, names.size()); - } - - @Test - public void test() { - doTest(-1); - } - - @Test - public void testSmallChildNodeBatchSize() { - doTest(1); - } - - private void doTest(int childNodeBatchSize) { - if (!isSimpleKernel(mk)) { - // TODO fix test since it incorrectly expects a specific order of child nodes - return; - } - - mk.commit("/", "+ \"source\": { \"id\": 1, \"plus\": 0, \"a\": { \"x\": 10, \"y\": 20 }, \"b\": {\"z\": 100}, \"d\":{} }", mk.getHeadRevision(), ""); - Sync sync = new Sync(); - if (childNodeBatchSize > 0) { - sync.setChildNodesPerBatch(childNodeBatchSize); - } - String head = mk.getHeadRevision(); - sync.setSource(mk, head, "/"); - String diff = syncToString(sync); - assertEquals( - "add /source\n" + - "setProperty /source id=1\n" + - "setProperty /source plus=0\n" + - "add /source/a\n" + - "setProperty /source/a x=10\n" + - "setProperty /source/a y=20\n" + - "add /source/b\n" + - "setProperty /source/b z=100\n" + - "add /source/d\n", - diff); - - mk.commit("/", "+ \"target\": { \"id\": 2, \"minus\": 0, \"a\": { \"x\": 10 }, \"c\": {} }", mk.getHeadRevision(), ""); - head = mk.getHeadRevision(); - sync.setSource(mk, head, "/source"); - sync.setTarget(mk, head, "/target"); - diff = syncToString(sync); - assertEquals( - "setProperty /target id=1\n" + - "setProperty /target plus=0\n" + - "setProperty /target minus=null\n" + - "setProperty /target/a y=20\n" + - "add /target/b\n" + - "setProperty /target/b z=100\n" + - "add /target/d\n" + - "remove /target/c\n", diff); - - sync.setSource(mk, head, "/notExist"); - sync.setTarget(mk, head, "/target"); - diff = syncToString(sync); - assertEquals( - "remove /target\n", diff); - - sync.setSource(mk, head, "/notExist"); - sync.setTarget(mk, head, "/notExist2"); - diff = syncToString(sync); - assertEquals("", diff); - - } - - private static String syncToString(Sync sync) { - final StringBuilder buff = new StringBuilder(); - sync.run(new Sync.Handler() { - - @Override - public void addNode(String targetPath) { - buff.append("add ").append(targetPath).append('\n'); - } - - @Override - public void removeNode(String targetPath) { - buff.append("remove ").append(targetPath).append('\n'); - } - - @Override - public void setProperty(String targetPath, String property, String value) { - buff.append("setProperty ").append(targetPath).append(' '). - append(property).append('=').append(value).append('\n'); - } - }); - return buff.toString(); - - } - -} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/SynchronizedVerifierTest.java b/oak-core/src/test/java/org/apache/jackrabbit/mk/util/SynchronizedVerifierTest.java deleted file mode 100644 index 50def1e5998..00000000000 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/SynchronizedVerifierTest.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.mk.util; - -import java.util.concurrent.atomic.AtomicInteger; -import junit.framework.TestCase; -import org.apache.jackrabbit.mk.util.Concurrent.Task; - -/** - * Tests the SynchronizedVerifier - */ -public class SynchronizedVerifierTest extends TestCase { - - public void testReadRead() throws Exception { - final AtomicInteger x = new AtomicInteger(); - SynchronizedVerifier.setDetect(AtomicInteger.class, true); - Concurrent.run("read", new Task() { - public void call() throws Exception { - SynchronizedVerifier.check(x, false); - x.get(); - } - }); - SynchronizedVerifier.setDetect(AtomicInteger.class, false); - } - - public void testReadWrite() throws Exception { - final AtomicInteger x = new AtomicInteger(); - SynchronizedVerifier.setDetect(AtomicInteger.class, true); - try { - Concurrent.run("readWrite", new Task() { - public void call() throws Exception { - if (Thread.currentThread().getName().endsWith("1")) { - SynchronizedVerifier.check(x, true); - x.set(1); - } else { - SynchronizedVerifier.check(x, false); - x.get(); - } - } - }); - fail(); - } catch (AssertionError e) { - // expected - } finally { - SynchronizedVerifier.setDetect(AtomicInteger.class, false); - } - } - - public void testWriteWrite() throws Exception { - final AtomicInteger x = new AtomicInteger(); - SynchronizedVerifier.setDetect(AtomicInteger.class, true); - try { - Concurrent.run("write", new Task() { - public void call() throws Exception { - SynchronizedVerifier.check(x, true); - x.set(1); - } - }); - fail(); - } catch (AssertionError e) { - // expected - } finally { - SynchronizedVerifier.setDetect(AtomicInteger.class, false); - } - } - -} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/OakAssert.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/OakAssert.java new file mode 100644 index 00000000000..e041ba80eba --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/OakAssert.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.apache.jackrabbit.oak.api.Tree; + +import com.google.common.collect.Lists; + +public class OakAssert { + + public static void assertSequence(Iterable trees, String... names) { + List expected = Lists.newArrayList(names); + List actual = Lists.newArrayList(); + for (Tree t : trees) { + actual.add(t.getName()); + } + assertEquals(expected.toString(), actual.toString()); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/api/ContentSessionTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/api/ContentSessionTest.java new file mode 100644 index 00000000000..218511d32f0 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/api/ContentSessionTest.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.api; + +import java.io.IOException; + +import javax.jcr.NoSuchWorkspaceException; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.plugins.commit.AnnotatingConflictHandler; +import org.apache.jackrabbit.oak.plugins.commit.ConflictValidator; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class ContentSessionTest { + + private ContentRepository repository; + + @Before + public void setUp() { + repository = new Oak() + .with(new ConflictValidator()) + .with(new AnnotatingConflictHandler()) + .createContentRepository(); + } + + @After + public void tearDown() { + repository = null; + } + + @Test(expected = IllegalStateException.class) + public void throwOnClosedSession() throws LoginException, NoSuchWorkspaceException, IOException { + ContentSession session = repository.login(null, null); + session.close(); + session.getLatestRoot(); + } + + @Test(expected = IllegalStateException.class) + public void throwOnClosedRoot() throws LoginException, NoSuchWorkspaceException, IOException { + ContentSession session = repository.login(null, null); + Root root = session.getLatestRoot(); + session.close(); + root.getTree("/"); + } + + @Test(expected = IllegalStateException.class) + public void throwOnClosedTree() throws LoginException, NoSuchWorkspaceException, IOException { + ContentSession session = repository.login(null, null); + Root root = session.getLatestRoot(); + Tree tree = root.getTree("/"); + session.close(); + tree.getChild("any"); + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/api/QueryTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/api/QueryTest.java new file mode 100644 index 00000000000..575d9476064 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/api/QueryTest.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.api; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.query.Query; + +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; + +/** + * QueryTest contains query related tests. + */ +public class QueryTest { + + private ContentRepository repository; + + @Before + public void setUp() { + repository = new Oak().createContentRepository(); + } + + @After + public void tearDown() { + repository = null; + } + + @Test + public void queryOnStableRevision() throws Exception { + ContentSession s = repository.login(null, null); + Root r = s.getLatestRoot(); + Tree t = r.getTree("/"); + t.addChild("node1").setProperty("jcr:primaryType", "nt:base"); + t.addChild("node2").setProperty("jcr:primaryType", "nt:base"); + t.addChild("node3").setProperty("jcr:primaryType", "nt:base"); + r.commit(); + + ContentSession s2 = repository.login(null, null); + Root r2 = s2.getLatestRoot(); + + r.getTree("/").getChild("node2").remove(); + r.commit(); + + Result result = r2.getQueryEngine().executeQuery( + "//element(*, nt:base)", + Query.XPATH, Long.MAX_VALUE, 0, + Collections.emptyMap(), + NamePathMapper.DEFAULT); + Set paths = new HashSet(); + for (ResultRow rr : result.getRows()) { + paths.add(rr.getPath()); + } + assertEquals(new HashSet(Arrays.asList("/", "/node1", "/node2", "/node3")), paths); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/api/RootTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/api/RootTest.java new file mode 100644 index 00000000000..4f2215707a1 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/api/RootTest.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.api; + +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.plugins.commit.AnnotatingConflictHandler; +import org.apache.jackrabbit.oak.plugins.commit.ConflictValidator; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.apache.jackrabbit.oak.OakAssert.assertSequence; + +/** + * Contains tests related to {@link Root} + */ +public class RootTest { + + private ContentRepository repository; + + @Before + public void setUp() { + repository = new Oak() + .with(new ConflictValidator()) + .with(new AnnotatingConflictHandler()) + .createContentRepository(); + } + + @After + public void tearDown() { + repository = null; + } + + @Test + public void copyOrderableNodes() throws Exception { + ContentSession s = repository.login(null, null); + try { + Root r = s.getLatestRoot(); + Tree t = r.getTree("/"); + Tree c = t.addChild("c"); + c.addChild("node1").orderBefore(null); + c.addChild("node2"); + t.addChild("node3"); + r.commit(); + + r.copy("/node3", "/c/node3"); + c = r.getTree("/").getChild("c"); + assertSequence(c.getChildren(), "node1", "node2", "node3"); + r.commit(); + c = r.getTree("/").getChild("c"); + assertSequence(c.getChildren(), "node1", "node2", "node3"); + } finally { + s.close(); + } + } + + @Test + public void moveOrderableNodes() throws Exception { + ContentSession s = repository.login(null, null); + try { + Root r = s.getLatestRoot(); + Tree t = r.getTree("/"); + Tree c = t.addChild("c"); + c.addChild("node1").orderBefore(null); + c.addChild("node2"); + t.addChild("node3"); + r.commit(); + + r.move("/node3", "/c/node3"); + c = r.getTree("/").getChild("c"); + assertSequence(c.getChildren(), "node1", "node2", "node3"); + r.commit(); + c = r.getTree("/").getChild("c"); + assertSequence(c.getChildren(), "node1", "node2", "node3"); + } finally { + s.close(); + } + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/api/TreeTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/api/TreeTest.java new file mode 100644 index 00000000000..c2bbe5fe434 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/api/TreeTest.java @@ -0,0 +1,504 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.api; + +import java.util.Set; + +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.plugins.commit.AnnotatingConflictHandler; +import org.apache.jackrabbit.oak.plugins.commit.ConflictValidator; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.google.common.collect.Sets; + +import static org.apache.jackrabbit.oak.OakAssert.assertSequence; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Contains tests related to {@link Tree} + */ +public class TreeTest { + + private ContentRepository repository; + + @Before + public void setUp() { + repository = new Oak() + .with(new ConflictValidator()) + .with(new AnnotatingConflictHandler() { + + /** + * Allow deleting changed node. + * See {@link TreeTest#removeWithConcurrentOrderBefore()} + */ + @Override + public Resolution deleteChangedNode(NodeBuilder parent, + String name, + NodeState theirs) { + return Resolution.OURS; + } + }) + .createContentRepository(); + } + + @After + public void tearDown() { + repository = null; + } + + @Test + public void orderBefore() throws Exception { + ContentSession s = repository.login(null, null); + try { + Root r = s.getLatestRoot(); + Tree t = r.getTree("/"); + t.addChild("node1"); + t.addChild("node2"); + t.addChild("node3"); + r.commit(); + t = r.getTree("/"); + t.getChild("node1").orderBefore("node2"); + t.getChild("node3").orderBefore(null); + assertSequence(t.getChildren(), "node1", "node2", "node3"); + r.commit(); + // check again after commit + t = r.getTree("/"); + assertSequence(t.getChildren(), "node1", "node2", "node3"); + + t.getChild("node3").orderBefore("node2"); + assertSequence(t.getChildren(), "node1", "node3", "node2"); + r.commit(); + t = r.getTree("/"); + assertSequence(t.getChildren(), "node1", "node3", "node2"); + + t.getChild("node1").orderBefore(null); + assertSequence(t.getChildren(), "node3", "node2", "node1"); + r.commit(); + t = r.getTree("/"); + assertSequence(t.getChildren(), "node3", "node2", "node1"); + + // :childOrder property invisible? + assertTrue(t.getProperty(":childOrder") == null); + assertEquals("must not have any properties", 0, t.getPropertyCount()); + } finally { + s.close(); + } + } + + @Test + public void concurrentOrderBefore() throws Exception { + ContentSession s1 = repository.login(null, null); + try { + Root r1 = s1.getLatestRoot(); + Tree t1 = r1.getTree("/"); + t1.addChild("node1"); + t1.addChild("node2"); + t1.addChild("node3"); + r1.commit(); + t1 = r1.getTree("/"); + + ContentSession s2 = repository.login(null, null); + try { + Root r2 = s2.getLatestRoot(); + Tree t2 = r2.getTree("/"); + + t1.getChild("node2").orderBefore("node1"); + t1.getChild("node3").orderBefore(null); + r1.commit(); + t1 = r1.getTree("/"); + assertSequence(t1.getChildren(), "node2", "node1", "node3"); + + t2.getChild("node3").orderBefore("node1"); + t2.getChild("node2").orderBefore(null); + r2.commit(); + t2 = r2.getTree("/"); + // other session wins + assertSequence(t2.getChildren(), "node2", "node1", "node3"); + + // try again on current root + t2.getChild("node3").orderBefore("node1"); + t2.getChild("node2").orderBefore(null); + r2.commit(); + t2 = r2.getTree("/"); + assertSequence(t2.getChildren(), "node3", "node1", "node2"); + + } finally { + s2.close(); + } + } finally { + s1.close(); + } + } + + @Test + public void concurrentOrderBeforeWithAdd() throws Exception { + ContentSession s1 = repository.login(null, null); + try { + Root r1 = s1.getLatestRoot(); + Tree t1 = r1.getTree("/"); + t1.addChild("node1"); + t1.addChild("node2"); + t1.addChild("node3"); + r1.commit(); + t1 = r1.getTree("/"); + + ContentSession s2 = repository.login(null, null); + try { + Root r2 = s2.getLatestRoot(); + Tree t2 = r2.getTree("/"); + + t1.getChild("node2").orderBefore("node1"); + t1.getChild("node3").orderBefore(null); + t1.addChild("node4"); + r1.commit(); + t1 = r1.getTree("/"); + assertSequence(t1.getChildren(), "node2", "node1", "node3", "node4"); + + t2.getChild("node3").orderBefore("node1"); + r2.commit(); + t2 = r2.getTree("/"); + // other session wins + assertSequence(t2.getChildren(), "node2", "node1", "node3", "node4"); + + // try again on current root + t2.getChild("node3").orderBefore("node1"); + r2.commit(); + t2 = r2.getTree("/"); + assertSequence(t2.getChildren(), "node2", "node3", "node1", "node4"); + + } finally { + s2.close(); + } + } finally { + s1.close(); + } + } + + @Test + public void concurrentOrderBeforeWithRemove() throws Exception { + ContentSession s1 = repository.login(null, null); + try { + Root r1 = s1.getLatestRoot(); + Tree t1 = r1.getTree("/"); + t1.addChild("node1"); + t1.addChild("node2"); + t1.addChild("node3"); + t1.addChild("node4"); + r1.commit(); + t1 = r1.getTree("/"); + + ContentSession s2 = repository.login(null, null); + try { + Root r2 = s2.getLatestRoot(); + Tree t2 = r2.getTree("/"); + + t1.getChild("node2").orderBefore("node1"); + t1.getChild("node3").orderBefore(null); + t1.getChild("node4").remove(); + r1.commit(); + t1 = r1.getTree("/"); + assertSequence(t1.getChildren(), "node2", "node1", "node3"); + + t2.getChild("node3").orderBefore("node1"); + r2.commit(); + t2 = r2.getTree("/"); + // other session wins + assertSequence(t2.getChildren(), "node2", "node1", "node3"); + + // try again on current root + t2.getChild("node3").orderBefore("node1"); + r2.commit(); + t2 = r2.getTree("/"); + assertSequence(t2.getChildren(), "node2", "node3", "node1"); + + } finally { + s2.close(); + } + } finally { + s1.close(); + } + } + + @Test + public void concurrentOrderBeforeWithRemoveOtherSession() throws Exception { + ContentSession s1 = repository.login(null, null); + try { + Root r1 = s1.getLatestRoot(); + Tree t1 = r1.getTree("/"); + t1.addChild("node1").orderBefore(null); + t1.addChild("node2"); + t1.addChild("node3"); + t1.addChild("node4"); + r1.commit(); + t1 = r1.getTree("/"); + + ContentSession s2 = repository.login(null, null); + try { + Root r2 = s2.getLatestRoot(); + Tree t2 = r2.getTree("/"); + + t1.getChild("node2").orderBefore("node1"); + t1.getChild("node3").orderBefore(null); + r1.commit(); + t1 = r1.getTree("/"); + assertSequence(t1.getChildren(), "node2", "node1", "node4", "node3"); + + t2.getChild("node3").orderBefore("node1"); + t2.getChild("node4").remove(); + r2.commit(); + t2 = r2.getTree("/"); + // other session wins wrt ordering, but node4 is gone + assertSequence(t2.getChildren(), "node2", "node1", "node3"); + + // try reorder again on current root + t2.getChild("node3").orderBefore("node1"); + r2.commit(); + t2 = r2.getTree("/"); + assertSequence(t2.getChildren(), "node2", "node3", "node1"); + + } finally { + s2.close(); + } + } finally { + s1.close(); + } + } + + @Test + public void concurrentOrderBeforeRemoved() throws Exception { + ContentSession s1 = repository.login(null, null); + try { + Root r1 = s1.getLatestRoot(); + Tree t1 = r1.getTree("/"); + t1.addChild("node1"); + t1.addChild("node2"); + t1.addChild("node3"); + r1.commit(); + t1 = r1.getTree("/"); + + ContentSession s2 = repository.login(null, null); + try { + Root r2 = s2.getLatestRoot(); + Tree t2 = r2.getTree("/"); + + t1.getChild("node2").orderBefore("node1"); + t1.getChild("node3").remove(); + r1.commit(); + t1 = r1.getTree("/"); + assertSequence(t1.getChildren(), "node2", "node1"); + + t2.getChild("node3").orderBefore("node1"); + r2.commit(); + t2 = r2.getTree("/"); + assertSequence(t2.getChildren(), "node2", "node1"); + + } finally { + s2.close(); + } + } finally { + s1.close(); + } + } + + @Test + public void concurrentOrderBeforeAllRemoved() throws Exception { + ContentSession s1 = repository.login(null, null); + try { + Root r1 = s1.getLatestRoot(); + Tree t1 = r1.getTree("/").addChild("c"); + t1.addChild("node1").orderBefore(null); + t1.addChild("node2"); + t1.addChild("node3"); + r1.commit(); + t1 = r1.getTree("/c"); + + ContentSession s2 = repository.login(null, null); + try { + Root r2 = s2.getLatestRoot(); + Tree t2 = r2.getTree("/c"); + + t1.remove(); + // now 'c' does not have ordered children anymore + r1.getTree("/").addChild("c"); + r1.commit(); + t1 = r1.getTree("/c"); + assertSequence(t1.getChildren()); + + t2.getChild("node3").orderBefore("node1"); + r2.commit(); + t2 = r2.getTree("/c"); + assertSequence(t2.getChildren()); + + } finally { + s2.close(); + } + } finally { + s1.close(); + } + } + + @Test + public void concurrentOrderBeforeTargetRemoved() throws Exception { + ContentSession s1 = repository.login(null, null); + try { + Root r1 = s1.getLatestRoot(); + Tree t1 = r1.getTree("/"); + t1.addChild("node1").orderBefore(null); + t1.addChild("node2"); + t1.addChild("node3"); + t1.addChild("node4"); + r1.commit(); + t1 = r1.getTree("/"); + + ContentSession s2 = repository.login(null, null); + try { + Root r2 = s2.getLatestRoot(); + Tree t2 = r2.getTree("/"); + + t1.getChild("node2").orderBefore("node1"); + t1.getChild("node3").remove(); + r1.commit(); + t1 = r1.getTree("/"); + assertSequence(t1.getChildren(), "node2", "node1", "node4"); + + t2.getChild("node4").orderBefore("node3"); + r2.commit(); + t2 = r2.getTree("/"); + assertSequence(t2.getChildren(), "node2", "node1", "node4"); + + } finally { + s2.close(); + } + } finally { + s1.close(); + } + } + + @Test + public void concurrentAddChildOrderable() throws Exception { + ContentSession s1 = repository.login(null, null); + try { + Root r1 = s1.getLatestRoot(); + Tree t1 = r1.getTree("/"); + t1.addChild("node1").orderBefore(null); + t1.addChild("node2"); + r1.commit(); + ContentSession s2 = repository.login(null, null); + try { + Root r2 = s2.getLatestRoot(); + Tree t2 = r2.getTree("/"); + + t1 = r1.getTree("/"); + // node3 from s1 + t1.addChild("node3"); + r1.commit(); + + // node4 from s2 + t2.addChild("node4"); + r2.commit(); + + t1 = s1.getLatestRoot().getTree("/"); + assertSequence( + t1.getChildren(), "node1", "node2", "node3", "node4"); + } finally { + s2.close(); + } + } finally { + s1.close(); + } + + } + + @Test + public void concurrentAddChild() throws Exception { + ContentSession s1 = repository.login(null, null); + try { + Root r1 = s1.getLatestRoot(); + Tree t1 = r1.getTree("/"); + t1.addChild("node1"); + t1.addChild("node2"); + t1.addChild("node3"); + r1.commit(); + ContentSession s2 = repository.login(null, null); + try { + Root r2 = s2.getLatestRoot(); + Tree t2 = r2.getTree("/"); + + t1 = r1.getTree("/"); + // node4 from s1 + t1.addChild("node4"); + r1.commit(); + + // node5 from s2 + t2.addChild("node5"); + r2.commit(); + + r1 = s1.getLatestRoot(); + t1 = r1.getTree("/"); + Set names = Sets.newHashSet(); + for (Tree t : t1.getChildren()) { + names.add(t.getName()); + } + assertEquals(Sets.newHashSet("node1", "node2", "node3", "node4", "node5"), names); + } finally { + s2.close(); + } + } finally { + s1.close(); + } + } + + @Test + public void removeWithConcurrentOrderBefore() throws Exception { + ContentSession s1 = repository.login(null, null); + try { + Root r1 = s1.getLatestRoot(); + Tree t1 = r1.getTree("/").addChild("c"); + t1.addChild("node1").orderBefore(null); + t1.addChild("node2"); + r1.commit(); + ContentSession s2 = repository.login(null, null); + try { + Root r2 = s2.getLatestRoot(); + Tree t2 = r2.getTree("/c"); + + t1 = r1.getTree("/c"); + t1.getChild("node2").orderBefore("node1"); + r1.commit(); + t1 = r1.getTree("/c"); + assertSequence(t1.getChildren(), "node2", "node1"); + + t2.remove(); + r2.commit(); + assertFalse(r2.getTree("/").hasChild("c")); + + } finally { + s2.close(); + } + } finally { + s1.close(); + } + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/api/UniquePropertyTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/api/UniquePropertyTest.java new file mode 100644 index 00000000000..88f63c1cf7e --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/api/UniquePropertyTest.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.api; + +import java.util.UUID; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.plugins.index.IndexHookManager; +import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexHookProvider; +import org.apache.jackrabbit.oak.plugins.nodetype.InitialContent; +import org.apache.jackrabbit.oak.util.NodeUtil; +import org.junit.Test; + +import static org.junit.Assert.fail; + +/** + * UniquePropertyTest... + */ +public class UniquePropertyTest { + + @Test + public void testUniqueness() throws CommitFailedException { + + Root root = new Oak() + .with(new IndexHookManager(new PropertyIndexHookProvider())) + .with(new InitialContent()).createRoot(); + + NodeUtil node = new NodeUtil(root.getTree("/")); + String uuid = UUID.randomUUID().toString(); + node.setString(JcrConstants.JCR_UUID, uuid); + root.commit(); + + NodeUtil child = new NodeUtil(root.getTree("/")).addChild("another", "rep:User"); + child.setString(JcrConstants.JCR_UUID, uuid); + try { + root.commit(); + fail("Duplicate jcr:uuid should be detected."); + } catch (CommitFailedException e) { + // expected + } + } + +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/core/DefaultConflictHandlerTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/core/DefaultConflictHandlerTest.java new file mode 100644 index 00000000000..c13daf46297 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/core/DefaultConflictHandlerTest.java @@ -0,0 +1,254 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.core; + +import static org.apache.jackrabbit.oak.api.Type.STRING; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.ContentSession; +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.plugins.commit.DefaultConflictHandler; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class DefaultConflictHandlerTest { + + private static final String OUR_VALUE = "foo"; + private static final String THEIR_VALUE = "bar"; + + private RootImpl ourRoot; + private Root theirRoot; + + @Before + public void setUp() throws CommitFailedException { + ContentSession session = new Oak().createContentSession(); + + // Add test content + Root root = session.getLatestRoot(); + Tree tree = root.getTree("/"); + tree.setProperty("a", 1); + tree.setProperty("b", 2); + tree.setProperty("c", 3); + tree.addChild("x"); + tree.addChild("y"); + tree.addChild("z"); + root.commit(); + + ourRoot = (RootImpl) session.getLatestRoot(); + theirRoot = session.getLatestRoot(); + } + + @After + public void tearDown() { + ourRoot = null; + theirRoot = null; + } + + @Test + public void testAddExistingPropertyOurs() throws CommitFailedException { + theirRoot.getTree("/").setProperty("p", THEIR_VALUE); + ourRoot.getTree("/").setProperty("p", OUR_VALUE); + + theirRoot.commit(); + ourRoot.commit(); + + PropertyState p = ourRoot.getTree("/").getProperty("p"); + assertNotNull(p); + assertEquals(OUR_VALUE, p.getValue(STRING)); + } + + @Test + public void testChangeDeletedPropertyOurs() throws CommitFailedException { + theirRoot.getTree("/").removeProperty("a"); + ourRoot.getTree("/").setProperty("a", OUR_VALUE); + + theirRoot.commit(); + ourRoot.commit(); + + PropertyState p = ourRoot.getTree("/").getProperty("a"); + assertNotNull(p); + assertEquals(OUR_VALUE, p.getValue(STRING)); + } + + @Test + public void testChangeChangedPropertyOurs() throws CommitFailedException { + theirRoot.getTree("/").setProperty("a", THEIR_VALUE); + ourRoot.getTree("/").setProperty("a", OUR_VALUE); + + theirRoot.commit(); + ourRoot.commit(); + + PropertyState p = ourRoot.getTree("/").getProperty("a"); + assertNotNull(p); + assertEquals(OUR_VALUE, p.getValue(STRING)); + } + + @Test + public void testDeleteChangedPropertyOurs() throws CommitFailedException { + theirRoot.getTree("/").setProperty("a", THEIR_VALUE); + ourRoot.getTree("/").removeProperty("a"); + + theirRoot.commit(); + ourRoot.commit(); + + PropertyState p = ourRoot.getTree("/").getProperty("a"); + assertNull(p); + } + + @Test + public void testAddExistingNodeOurs() throws CommitFailedException { + theirRoot.getTree("/").addChild("n").setProperty("p", THEIR_VALUE); + ourRoot.getTree("/").addChild("n").setProperty("p", OUR_VALUE); + + theirRoot.commit(); + ourRoot.commit(); + + Tree n = ourRoot.getTree("/n"); + assertNotNull(n); + assertEquals(OUR_VALUE, n.getProperty("p").getValue(STRING)); + } + + @Test + public void testChangeDeletedNodeOurs() throws CommitFailedException { + theirRoot.getTree("/x").remove(); + ourRoot.getTree("/x").setProperty("p", OUR_VALUE); + + theirRoot.commit(); + ourRoot.commit(); + + Tree n = ourRoot.getTree("/x"); + assertNotNull(n); + assertEquals(OUR_VALUE, n.getProperty("p").getValue(STRING)); + } + + @Test + public void testDeleteChangedNodeOurs() throws CommitFailedException { + theirRoot.getTree("/x").setProperty("p", THEIR_VALUE); + ourRoot.getTree("/x").remove(); + + theirRoot.commit(); + ourRoot.commit(); + + Tree n = ourRoot.getTree("/x"); + assertNull(n); + } + + @Test + public void testAddExistingPropertyTheirs() throws CommitFailedException { + theirRoot.getTree("/").setProperty("p", THEIR_VALUE); + ourRoot.getTree("/").setProperty("p", OUR_VALUE); + + theirRoot.commit(); + ourRoot.setConflictHandler(DefaultConflictHandler.THEIRS); + ourRoot.commit(); + + PropertyState p = ourRoot.getTree("/").getProperty("p"); + assertNotNull(p); + assertEquals(THEIR_VALUE, p.getValue(STRING)); + } + + @Test + public void testChangeDeletedPropertyTheirs() throws CommitFailedException { + theirRoot.getTree("/").removeProperty("a"); + ourRoot.getTree("/").setProperty("a", OUR_VALUE); + + theirRoot.commit(); + ourRoot.setConflictHandler(DefaultConflictHandler.THEIRS); + ourRoot.commit(); + + PropertyState p = ourRoot.getTree("/").getProperty("a"); + assertNull(p); + } + + @Test + public void testChangeChangedPropertyTheirs() throws CommitFailedException { + theirRoot.getTree("/").setProperty("a", THEIR_VALUE); + ourRoot.getTree("/").setProperty("a", OUR_VALUE); + + theirRoot.commit(); + ourRoot.setConflictHandler(DefaultConflictHandler.THEIRS); + ourRoot.commit(); + + PropertyState p = ourRoot.getTree("/").getProperty("a"); + assertNotNull(p); + assertEquals(THEIR_VALUE, p.getValue(STRING)); + } + + @Test + public void testDeleteChangedPropertyTheirs() throws CommitFailedException { + theirRoot.getTree("/").setProperty("a", THEIR_VALUE); + ourRoot.getTree("/").removeProperty("a"); + + theirRoot.commit(); + ourRoot.setConflictHandler(DefaultConflictHandler.THEIRS); + ourRoot.commit(); + + PropertyState p = ourRoot.getTree("/").getProperty("a"); + assertNotNull(p); + assertEquals(THEIR_VALUE, p.getValue(STRING)); + } + + @Test + public void testAddExistingNodeTheirs() throws CommitFailedException { + theirRoot.getTree("/").addChild("n").setProperty("p", THEIR_VALUE); + ourRoot.getTree("/").addChild("n").setProperty("p", OUR_VALUE); + + theirRoot.commit(); + ourRoot.setConflictHandler(DefaultConflictHandler.THEIRS); + ourRoot.commit(); + + Tree n = ourRoot.getTree("/n"); + assertNotNull(n); + assertEquals(THEIR_VALUE, n.getProperty("p").getValue(STRING)); + } + + @Test + public void testChangeDeletedNodeTheirs() throws CommitFailedException { + theirRoot.getTree("/x").remove(); + ourRoot.getTree("/x").setProperty("p", OUR_VALUE); + + theirRoot.commit(); + ourRoot.setConflictHandler(DefaultConflictHandler.THEIRS); + ourRoot.commit(); + + Tree n = ourRoot.getTree("/x"); + assertNull(n); + } + + @Test + public void testDeleteChangedNodeTheirs() throws CommitFailedException { + theirRoot.getTree("/x").setProperty("p", THEIR_VALUE); + ourRoot.getTree("/").remove(); + + theirRoot.commit(); + ourRoot.commit(); + + Tree n = ourRoot.getTree("/x"); + assertNotNull(n); + assertEquals(THEIR_VALUE, n.getProperty("p").getValue(STRING)); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/core/RootImplFuzzIT.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/core/RootImplFuzzIT.java new file mode 100644 index 00000000000..0bb53771466 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/core/RootImplFuzzIT.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.core; + +import java.util.Iterator; +import java.util.Random; + +import javax.security.auth.Subject; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.core.RootImplFuzzIT.Operation.Rebase; +import org.apache.jackrabbit.oak.kernel.KernelNodeStore; +import org.apache.jackrabbit.oak.security.authorization.AccessControlProviderImpl; +import org.apache.jackrabbit.oak.spi.query.CompositeQueryIndexProvider; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.jackrabbit.oak.core.RootImplFuzzIT.Operation.AddNode; +import static org.apache.jackrabbit.oak.core.RootImplFuzzIT.Operation.CopyNode; +import static org.apache.jackrabbit.oak.core.RootImplFuzzIT.Operation.MoveNode; +import static org.apache.jackrabbit.oak.core.RootImplFuzzIT.Operation.RemoveNode; +import static org.apache.jackrabbit.oak.core.RootImplFuzzIT.Operation.RemoveProperty; +import static org.apache.jackrabbit.oak.core.RootImplFuzzIT.Operation.Save; +import static org.apache.jackrabbit.oak.core.RootImplFuzzIT.Operation.SetProperty; +import static org.junit.Assert.assertEquals; + +/** + * Fuzz test running random sequences of operations on {@link Tree}. + * Run with -DKernelRootFuzzIT-seed=42 to set a specific seed (i.e. 42); + */ +public class RootImplFuzzIT { + + static final Logger log = LoggerFactory.getLogger(RootImplFuzzIT.class); + + private static final int OP_COUNT = 5000; + + private static final int SEED = Integer.getInteger( + RootImplFuzzIT.class.getSimpleName() + "-seed", + new Random().nextInt()); + + private static final Random random = new Random(SEED); + + private KernelNodeStore store1; + private RootImpl root1; + + private KernelNodeStore store2; + private RootImpl root2; + + private int counter; + + @Before + public void setup() { + counter = 0; + + MicroKernel mk1 = new MicroKernelImpl("./target/mk1/" + random.nextInt()); + store1 = new KernelNodeStore(mk1); + mk1.commit("", "+\"/root\":{}", mk1.getHeadRevision(), ""); + root1 = new RootImpl(store1, null, new Subject(), + new AccessControlProviderImpl(), new CompositeQueryIndexProvider()); + + MicroKernel mk2 = new MicroKernelImpl("./target/mk2/" + random.nextInt()); + store2 = new KernelNodeStore(mk2); + mk2.commit("", "+\"/root\":{}", mk2.getHeadRevision(), ""); + root2 = new RootImpl(store2, null, new Subject(), + new AccessControlProviderImpl(), new CompositeQueryIndexProvider()); + } + + @Test + public void fuzzTest() throws Exception { + for (Operation op : operations(OP_COUNT)) { + log.info("{}", op); + op.apply(root1); + op.apply(root2); + checkEqual(root1.getTree("/"), root2.getTree("/")); + + root1.commit(); + checkEqual(root1.getTree("/"), root2.getTree("/")); + if (op instanceof Save) { + root2.commit(); + assertEquals("seed " + SEED, store1.getRoot(), store2.getRoot()); + } + } + } + + private Iterable operations(final int count) { + return new Iterable() { + int k = count; + + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + return k-- > 0; + } + + @Override + public Operation next() { + return createOperation(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + + abstract static class Operation { + abstract void apply(RootImpl root); + + static class AddNode extends Operation { + private final String parentPath; + private final String name; + + AddNode(String parentPath, String name) { + this.parentPath = parentPath; + this.name = name; + } + + @Override + void apply(RootImpl root) { + root.getTree(parentPath).addChild(name); + } + + @Override + public String toString() { + return '+' + PathUtils.concat(parentPath, name) + ":{}"; + } + } + + static class RemoveNode extends Operation { + private final String path; + + RemoveNode(String path) { + this.path = path; + } + + @Override + void apply(RootImpl root) { + String parentPath = PathUtils.getParentPath(path); + String name = PathUtils.getName(path); + root.getTree(parentPath).getChild(name).remove(); + } + + @Override + public String toString() { + return '-' + path; + } + } + + static class MoveNode extends Operation { + private final String source; + private final String destination; + + MoveNode(String source, String destParent, String destName) { + this.source = source; + destination = PathUtils.concat(destParent, destName); + } + + @Override + void apply(RootImpl root) { + root.move(source, destination); + } + + @Override + public String toString() { + return '>' + source + ':' + destination; + } + } + + static class CopyNode extends Operation { + private final String source; + private final String destination; + + CopyNode(String source, String destParent, String destName) { + this.source = source; + destination = PathUtils.concat(destParent, destName); + } + + @Override + void apply(RootImpl root) { + root.copy(source, destination); + } + + @Override + public String toString() { + return '*' + source + ':' + destination; + } + } + + static class SetProperty extends Operation { + private final String parentPath; + private final String propertyName; + private final String propertyValue; + + SetProperty(String parentPath, String name, String value) { + this.parentPath = parentPath; + this.propertyName = name; + this.propertyValue = value; + } + + @Override + void apply(RootImpl root) { + root.getTree(parentPath).setProperty(propertyName, propertyValue); + } + + @Override + public String toString() { + return '^' + PathUtils.concat(parentPath, propertyName) + ':' + + propertyValue; + } + } + + static class RemoveProperty extends Operation { + private final String parentPath; + private final String name; + + RemoveProperty(String parentPath, String name) { + this.parentPath = parentPath; + this.name = name; + } + + @Override + void apply(RootImpl root) { + root.getTree(parentPath).removeProperty(name); + } + + @Override + public String toString() { + return '^' + PathUtils.concat(parentPath, name) + ":null"; + } + } + + static class Save extends Operation { + @Override + void apply(RootImpl root) { + // empty + } + + @Override + public String toString() { + return "save"; + } + } + + static class Rebase extends Operation { + @Override + void apply(RootImpl root) { + root.rebase(); + } + + @Override + public String toString() { + return "rebase"; + } + } + } + + private Operation createOperation() { + Operation op; + do { + switch (random.nextInt(11)) { + case 0: + case 1: + case 2: + op = createAddNode(); + break; + case 3: + op = createRemoveNode(); + break; + case 4: + op = createMoveNode(); + break; + case 5: + // Too many copy ops make the test way slow + op = random.nextInt(10) == 0 ? createCopyNode() : null; + break; + case 6: + op = createAddProperty(); + break; + case 7: + op = createSetProperty(); + break; + case 8: + op = createRemoveProperty(); + break; + case 9: + op = new Save(); + break; + case 10: + op = new Rebase(); + break; + default: + throw new IllegalStateException(); + } + } while (op == null); + return op; + } + + private Operation createAddNode() { + String parentPath = chooseNodePath(); + String name = createNodeName(); + return new AddNode(parentPath, name); + } + + private Operation createRemoveNode() { + String path = chooseNodePath(); + return "/root".equals(path) ? null : new RemoveNode(path); + } + + private Operation createMoveNode() { + String source = chooseNodePath(); + String destParent = chooseNodePath(); + String destName = createNodeName(); + return "/root".equals(source) || destParent.startsWith(source) + ? null + : new MoveNode(source, destParent, destName); + } + + private Operation createCopyNode() { + String source = chooseNodePath(); + String destParent = chooseNodePath(); + String destName = createNodeName(); + return "/root".equals(source) + ? null + : new CopyNode(source, destParent, destName); + } + + private Operation createAddProperty() { + String parent = chooseNodePath(); + String name = createPropertyName(); + String value = createValue(); + return new SetProperty(parent, name, value); + } + + private Operation createSetProperty() { + String path = choosePropertyPath(); + if (path == null) { + return null; + } + String value = createValue(); + return new SetProperty(PathUtils.getParentPath(path), PathUtils.getName(path), value); + } + + private Operation createRemoveProperty() { + String path = choosePropertyPath(); + if (path == null) { + return null; + } + return new RemoveProperty(PathUtils.getParentPath(path), PathUtils.getName(path)); + } + + private String createNodeName() { + return "N" + counter++; + } + + private String createPropertyName() { + return "P" + counter++; + } + + private String chooseNodePath() { + String path = "/root"; + + String next; + while ((next = chooseNode(path)) != null) { + path = next; + } + + return path; + } + + private String choosePropertyPath() { + return chooseProperty(chooseNodePath()); + } + + private String chooseNode(String parentPath) { + Tree state = root1.getTree(parentPath); + + int k = random.nextInt((int) (state.getChildrenCount() + 1)); + int c = 0; + for (Tree child : state.getChildren()) { + if (c++ == k) { + return PathUtils.concat(parentPath, child.getName()); + } + } + + return null; + } + + private String chooseProperty(String parentPath) { + Tree state = root1.getTree(parentPath); + int k = random.nextInt((int) (state.getPropertyCount() + 1)); + int c = 0; + for (PropertyState entry : state.getProperties()) { + if (c++ == k) { + return PathUtils.concat(parentPath, entry.getName()); + } + } + return null; + } + + private String createValue() { + return ("V" + counter++); + } + + private static void checkEqual(Tree tree1, Tree tree2) { + String message = + tree1.getPath() + "!=" + tree2.getPath() + + " (seed " + SEED + ')'; + assertEquals(message, tree1.getPath(), tree2.getPath()); + assertEquals(message, tree1.getChildrenCount(), tree2.getChildrenCount()); + assertEquals(message, tree1.getPropertyCount(), tree2.getPropertyCount()); + + for (PropertyState property1 : tree1.getProperties()) { + PropertyState property2 = tree2.getProperty(property1.getName()); + assertEquals(message, property1, property2); + } + + for (Tree child1 : tree1.getChildren()) { + checkEqual(child1, tree2.getChild(child1.getName())); + } + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/core/RootImplTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/core/RootImplTest.java new file mode 100644 index 00000000000..456efe97d5c --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/core/RootImplTest.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.core; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +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; + +public class RootImplTest { + + private ContentSession session; + + @Before + public void setUp() throws CommitFailedException { + session = new Oak().createContentSession(); + + // Add test content + Root root = session.getLatestRoot(); + Tree tree = root.getTree("/"); + tree.setProperty("a", 1); + tree.setProperty("b", 2); + tree.setProperty("c", 3); + Tree x = tree.addChild("x"); + x.addChild("xx"); + x.setProperty("xa", "value"); + tree.addChild("y"); + tree.addChild("z"); + root.commit(); + } + + @After + public void tearDown() { + session = null; + } + + @Test + public void getTree() { + Root root = session.getLatestRoot(); + + List validPaths = new ArrayList(); + validPaths.add("/"); + validPaths.add("/x"); + validPaths.add("/x/xx"); + validPaths.add("/y"); + validPaths.add("/z"); + + for (String treePath : validPaths) { + Tree tree = root.getTree(treePath); + assertNotNull(tree); + assertEquals(treePath, tree.getPath()); + } + + List invalidPaths = new ArrayList(); + invalidPaths.add("/any"); + invalidPaths.add("/x/any"); + + for (String treePath : invalidPaths) { + assertNull(root.getTree(treePath)); + } + } + + @Test + public void move() throws CommitFailedException { + Root root = session.getLatestRoot(); + Tree tree = root.getTree("/"); + + Tree y = tree.getChild("y"); + + assertTrue(tree.hasChild("x")); + root.move("/x", "/y/xx"); + assertFalse(tree.hasChild("x")); + assertTrue(y.hasChild("xx")); + + root.commit(); + tree = root.getTree("/"); + + assertFalse(tree.hasChild("x")); + assertTrue(tree.hasChild("y")); + assertTrue(tree.getChild("y").hasChild("xx")); + } + + /** + * Regression test for OAK-208 + */ + @Test + public void removeMoved() throws CommitFailedException { + Root root = session.getLatestRoot(); + Tree r = root.getTree("/"); + r.addChild("a"); + r.addChild("b"); + + root.move("/a", "/b/c"); + assertFalse(r.hasChild("a")); + assertTrue(r.hasChild("b")); + + r.getChild("b").remove(); + assertFalse(r.hasChild("a")); + assertFalse(r.hasChild("b")); + + root.commit(); + assertFalse(r.hasChild("a")); + assertFalse(r.hasChild("b")); + } + + @Test + public void rename() throws CommitFailedException { + Root root = session.getLatestRoot(); + Tree tree = root.getTree("/"); + + assertTrue(tree.hasChild("x")); + root.move("/x", "/xx"); + assertFalse(tree.hasChild("x")); + assertTrue(tree.hasChild("xx")); + + root.commit(); + tree = root.getTree("/"); + + assertFalse(tree.hasChild("x")); + assertTrue(tree.hasChild("xx")); + } + + @Test + public void copy() throws CommitFailedException { + Root root = session.getLatestRoot(); + Tree tree = root.getTree("/"); + + Tree y = tree.getChild("y"); + + assertTrue(tree.hasChild("x")); + root.copy("/x", "/y/xx"); + assertTrue(tree.hasChild("x")); + assertTrue(y.hasChild("xx")); + + root.commit(); + tree = root.getTree("/"); + + assertTrue(tree.hasChild("x")); + assertTrue(tree.hasChild("y")); + assertTrue(tree.getChild("y").hasChild("xx")); + } + + @Test + public void deepCopy() throws CommitFailedException { + Root root = session.getLatestRoot(); + Tree tree = root.getTree("/"); + + Tree y = tree.getChild("y"); + + root.getTree("/x").addChild("x1"); + root.copy("/x", "/y/xx"); + assertTrue(y.hasChild("xx")); + assertTrue(y.getChild("xx").hasChild("x1")); + + root.commit(); + tree = root.getTree("/"); + + assertTrue(tree.hasChild("x")); + assertTrue(tree.hasChild("y")); + assertTrue(tree.getChild("y").hasChild("xx")); + assertTrue(tree.getChild("y").getChild("xx").hasChild("x1")); + + Tree x = tree.getChild("x"); + Tree xx = tree.getChild("y").getChild("xx"); + checkEqual(x, xx); + } + + @Test + public void rebase() throws CommitFailedException { + Root root1 = session.getLatestRoot(); + Root root2 = session.getLatestRoot(); + + checkEqual(root1.getTree("/"), root2.getTree("/")); + + root2.getTree("/").addChild("one").addChild("two").addChild("three") + .setProperty("p1", "V1"); + root2.commit(); + + root1.rebase(); + checkEqual(root1.getTree("/"), (root2.getTree("/"))); + + Tree one = root2.getTree("/one"); + one.getChild("two").remove(); + one.addChild("four"); + root2.commit(); + + root1.rebase(); + checkEqual(root1.getTree("/"), (root2.getTree("/"))); + } + + private static void checkEqual(Tree tree1, Tree tree2) { + assertEquals(tree1.getChildrenCount(), tree2.getChildrenCount()); + assertEquals(tree1.getPropertyCount(), tree2.getPropertyCount()); + + for (PropertyState property1 : tree1.getProperties()) { + assertEquals(property1, tree2.getProperty(property1.getName())); + } + + for (Tree child1 : tree1.getChildren()) { + checkEqual(child1, tree2.getChild(child1.getName())); + } + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/core/TreeImplTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/core/TreeImplTest.java new file mode 100644 index 00000000000..d3a3f36e9a2 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/core/TreeImplTest.java @@ -0,0 +1,365 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.core; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import com.google.common.collect.Sets; +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.ContentSession; +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.api.Tree.Status; +import org.apache.jackrabbit.oak.plugins.memory.LongPropertyState; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.apache.jackrabbit.oak.api.Type.LONG; +import static org.apache.jackrabbit.oak.api.Type.STRING; +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; + +/** + * TreeImplTest... + */ +public class TreeImplTest { + + private Root root; + + @Before + public void setUp() throws CommitFailedException { + ContentSession session = new Oak().createContentSession(); + + // Add test content + root = session.getLatestRoot(); + Tree tree = root.getTree("/"); + tree.setProperty("a", 1); + tree.setProperty("b", 2); + tree.setProperty("c", 3); + tree.addChild("x"); + tree.addChild("y"); + tree.addChild("z"); + root.commit(); + + // Acquire a fresh new root to avoid problems from lingering state + root = session.getLatestRoot(); + } + + @After + public void tearDown() { + root = null; + } + + @Test + public void getChild() { + Tree tree = root.getTree("/"); + + Tree child = tree.getChild("any"); + assertNull(child); + + child = tree.getChild("x"); + assertNotNull(child); + } + + @Test + public void getProperty() { + Tree tree = root.getTree("/"); + + PropertyState propertyState = tree.getProperty("any"); + assertNull(propertyState); + + propertyState = tree.getProperty("a"); + assertNotNull(propertyState); + assertFalse(propertyState.isArray()); + assertEquals(LONG, propertyState.getType()); + assertEquals(1, (long) propertyState.getValue(LONG)); + } + + @Test + public void getChildren() { + Tree tree = root.getTree("/"); + + Iterable children = tree.getChildren(); + + Set expectedPaths = new HashSet(); + Collections.addAll(expectedPaths, "/x", "/y", "/z"); + + for (Tree child : children) { + assertTrue(expectedPaths.remove(child.getPath())); + } + assertTrue(expectedPaths.isEmpty()); + + assertEquals(3, tree.getChildrenCount()); + } + + @Test + public void getProperties() { + Tree tree = root.getTree("/"); + + Set expectedProperties = Sets.newHashSet( + LongPropertyState.createLongProperty("a", 1L), + LongPropertyState.createLongProperty("b", 2L), + LongPropertyState.createLongProperty("c", 3L)); + + Iterable properties = tree.getProperties(); + for (PropertyState property : properties) { + assertTrue(expectedProperties.remove(property)); + } + + assertTrue(expectedProperties.isEmpty()); + assertEquals(3, tree.getPropertyCount()); + } + + @Test + public void addChild() throws CommitFailedException { + Tree tree = root.getTree("/"); + + assertFalse(tree.hasChild("new")); + Tree added = tree.addChild("new"); + assertNotNull(added); + assertEquals("new", added.getName()); + assertTrue(tree.hasChild("new")); + + root.commit(); + tree = root.getTree("/"); + + assertTrue(tree.hasChild("new")); + + tree.getChild("new").addChild("more"); + assertTrue(tree.getChild("new").hasChild("more")); + } + + @Test + public void addExistingChild() throws CommitFailedException { + Tree tree = root.getTree("/"); + + assertFalse(tree.hasChild("new")); + tree.addChild("new"); + + root.commit(); + tree = root.getTree("/"); + + assertTrue(tree.hasChild("new")); + Tree added = tree.addChild("new"); + assertNotNull(added); + assertEquals("new", added.getName()); + } + + @Test + public void removeChild() throws CommitFailedException { + Tree tree = root.getTree("/"); + + assertTrue(tree.hasChild("x")); + tree.getChild("x").remove(); + assertFalse(tree.hasChild("x")); + + root.commit(); + tree = root.getTree("/"); + + assertFalse(tree.hasChild("x")); + } + + @Test + public void setProperty() throws CommitFailedException { + Tree tree = root.getTree("/"); + + assertFalse(tree.hasProperty("new")); + tree.setProperty("new", "value"); + PropertyState property = tree.getProperty("new"); + assertNotNull(property); + assertEquals("new", property.getName()); + assertEquals("value", property.getValue(STRING)); + + root.commit(); + tree = root.getTree("/"); + + property = tree.getProperty("new"); + assertNotNull(property); + assertEquals("new", property.getName()); + assertEquals("value", property.getValue(STRING)); + } + + @Test + public void removeProperty() throws CommitFailedException { + Tree tree = root.getTree("/"); + + assertTrue(tree.hasProperty("a")); + tree.removeProperty("a"); + assertFalse(tree.hasProperty("a")); + + root.commit(); + tree = root.getTree("/"); + + assertFalse(tree.hasProperty("a")); + } + + @Test + public void getChildrenCount() { + Tree tree = root.getTree("/"); + + assertEquals(3, tree.getChildrenCount()); + + tree.getChild("x").remove(); + assertEquals(2, tree.getChildrenCount()); + + tree.addChild("a"); + assertEquals(3, tree.getChildrenCount()); + + tree.addChild("x"); + assertEquals(4, tree.getChildrenCount()); + } + + @Test + public void getPropertyCount() { + Tree tree = root.getTree("/"); + + assertEquals(3, tree.getPropertyCount()); + + tree.setProperty("a", "foo"); + assertEquals(3, tree.getPropertyCount()); + + tree.removeProperty("a"); + assertEquals(2, tree.getPropertyCount()); + + tree.setProperty("x", "foo"); + assertEquals(3, tree.getPropertyCount()); + + tree.setProperty("a", "foo"); + assertEquals(4, tree.getPropertyCount()); + } + + @Test + public void addAndRemoveProperty() throws CommitFailedException { + Tree tree = root.getTree("/"); + + tree.setProperty("P0", "V1"); + root.commit(); + tree = root.getTree("/"); + assertTrue(tree.hasProperty("P0")); + + tree.removeProperty("P0"); + root.commit(); + tree = root.getTree("/"); + assertFalse(tree.hasProperty("P0")); + } + + @Test + public void nodeStatus() throws CommitFailedException { + Tree tree = root.getTree("/"); + + tree.addChild("new"); + assertEquals(Tree.Status.NEW, tree.getChild("new").getStatus()); + root.commit(); + + tree = root.getTree("/"); + assertEquals(Tree.Status.EXISTING, tree.getChild("new").getStatus()); + Tree added = tree.getChild("new"); + added.addChild("another"); + assertEquals(Tree.Status.MODIFIED, tree.getChild("new").getStatus()); + root.commit(); + + tree = root.getTree("/"); + assertEquals(Tree.Status.EXISTING, tree.getChild("new").getStatus()); + tree.getChild("new").getChild("another").remove(); + assertEquals(Tree.Status.MODIFIED, tree.getChild("new").getStatus()); + root.commit(); + + tree = root.getTree("/"); + assertEquals(Tree.Status.EXISTING, tree.getChild("new").getStatus()); + assertNull(tree.getChild("new").getChild("another")); + + Tree x = root.getTree("/x"); + Tree y = x.addChild("y"); + x.remove(); + assertEquals(Status.REMOVED, x.getStatus()); + assertEquals(Status.REMOVED, y.getStatus()); + } + + @Test + public void propertyStatus() throws CommitFailedException { + Tree tree = root.getTree("/"); + + tree.setProperty("new", "value1"); + assertEquals(Tree.Status.NEW, tree.getPropertyStatus("new")); + root.commit(); + + tree = root.getTree("/"); + assertEquals(Tree.Status.EXISTING, tree.getPropertyStatus("new")); + tree.setProperty("new", "value2"); + assertEquals(Tree.Status.MODIFIED, tree.getPropertyStatus("new")); + root.commit(); + + tree = root.getTree("/"); + assertEquals(Tree.Status.EXISTING, tree.getPropertyStatus("new")); + tree.removeProperty("new"); + assertEquals(Tree.Status.REMOVED, tree.getPropertyStatus("new")); + root.commit(); + + tree = root.getTree("/"); + assertNull(tree.getPropertyStatus("new")); + + Tree x = root.getTree("/x"); + x.setProperty("y", "value1"); + x.remove(); + assertEquals(Status.REMOVED, x.getPropertyStatus("y")); + } + + @Test + public void noTransitiveModifiedStatus() throws CommitFailedException { + Tree tree = root.getTree("/"); + tree.addChild("one").addChild("two"); + root.commit(); + + tree = root.getTree("/"); + tree.getChild("one").getChild("two").addChild("three"); + assertEquals(Tree.Status.EXISTING, tree.getChild("one").getStatus()); + assertEquals(Tree.Status.MODIFIED, tree.getChild("one").getChild("two").getStatus()); + } + + @Test + public void largeChildList() throws CommitFailedException { + Tree tree = root.getTree("/"); + + Set added = new HashSet(); + + tree.addChild("large"); + tree = tree.getChild("large"); + for (int c = 0; c < 10000; c++) { + String name = "n" + c; + added.add(name); + tree.addChild(name); + } + + root.commit(); + tree = root.getTree("/"); + tree = tree.getChild("large"); + + for (Tree child : tree.getChildren()) { + assertTrue(added.remove(child.getName())); + } + + assertTrue(added.isEmpty()); + } +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/kernel/JsopDiffTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/kernel/JsopDiffTest.java new file mode 100644 index 00000000000..0a421ee25b0 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/kernel/JsopDiffTest.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.kernel; + +import com.google.common.collect.ImmutableMap; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.plugins.memory.BooleanPropertyState; +import org.apache.jackrabbit.oak.plugins.memory.DoublePropertyState; +import org.apache.jackrabbit.oak.plugins.memory.LongPropertyState; +import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeState; +import org.apache.jackrabbit.oak.plugins.memory.StringPropertyState; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; + +public class JsopDiffTest { + + @Test + public void testPropertyChanges() { + JsopDiff diff; + PropertyState before = StringPropertyState.stringProperty("foo", "bar"); + + diff = new JsopDiff(null); + diff.propertyAdded(before); + assertEquals("^\"/foo\":\"bar\"", diff.toString()); + + diff = new JsopDiff(null); + diff.propertyChanged(before, LongPropertyState.createLongProperty("foo", 123L)); + assertEquals("^\"/foo\":123", diff.toString()); + + diff = new JsopDiff(null); + diff.propertyChanged(before, DoublePropertyState.doubleProperty("foo", 1.23)); + assertEquals("^\"/foo\":\"dou:1.23\"", diff.toString()); // TODO: 1.23? + + diff = new JsopDiff(null); + diff.propertyChanged(before, BooleanPropertyState.booleanProperty("foo", true)); + assertEquals("^\"/foo\":true", diff.toString()); + + diff = new JsopDiff(null); + diff.propertyDeleted(before); + assertEquals("^\"/foo\":null", diff.toString()); + } + + @Test + public void testNodeChanges() { + JsopDiff diff; + NodeState before = MemoryNodeState.EMPTY_NODE; + NodeState after = new MemoryNodeState( + ImmutableMap.of( + "a", LongPropertyState.createLongProperty("a", 1L)), + ImmutableMap.of( + "x", MemoryNodeState.EMPTY_NODE)); + + + diff = new JsopDiff(null); + diff.childNodeAdded("test", before); + assertEquals("+\"/test\":{}", diff.toString()); + + diff = new JsopDiff(null); + diff.childNodeChanged("test", before, after); + assertEquals("^\"/test/a\":1+\"/test/x\":{}", diff.toString()); + + diff = new JsopDiff(null); + diff.childNodeDeleted("test", after); + assertEquals("-\"/test\"", diff.toString()); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/kernel/KernelNodeStateTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/kernel/KernelNodeStateTest.java new file mode 100644 index 00000000000..6d96bdbec16 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/kernel/KernelNodeStateTest.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.kernel; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStore; +import org.apache.jackrabbit.oak.spi.state.NodeStoreBranch; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; +import static org.apache.jackrabbit.oak.api.Type.LONG; + +public class KernelNodeStateTest { + + private NodeState state; + + @Before + public void setUp() throws CommitFailedException { + NodeStore store = new KernelNodeStore(new MicroKernelImpl()); + NodeStoreBranch branch = store.branch(); + + NodeBuilder builder = branch.getRoot().builder(); + builder.setProperty("a", 1); + builder.setProperty("b", 2); + builder.setProperty("c", 3); + builder.child("x"); + builder.child("y"); + builder.child("z"); + branch.setRoot(builder.getNodeState()); + + state = branch.merge(); + } + + @After + public void tearDown() { + state = null; + } + + @Test + public void testGetPropertyCount() { + assertEquals(3, state.getPropertyCount()); + } + + @Test + public void testGetProperty() { + assertEquals("a", state.getProperty("a").getName()); + assertEquals(1, (long) state.getProperty("a").getValue(LONG)); + assertEquals("b", state.getProperty("b").getName()); + assertEquals(2, (long) state.getProperty("b").getValue(LONG)); + assertEquals("c", state.getProperty("c").getName()); + assertEquals(3, (long) state.getProperty("c").getValue(LONG)); + assertNull(state.getProperty("x")); + } + + @Test + public void testGetProperties() { + List names = new ArrayList(); + List values = new ArrayList(); + for (PropertyState property : state.getProperties()) { + names.add(property.getName()); + values.add(property.getValue(LONG)); + } + Collections.sort(names); + Collections.sort(values); + assertEquals(Arrays.asList("a", "b", "c"), names); + assertEquals(Arrays.asList(1L, 2L, 3L), values); + } + + @Test + public void testGetChildNodeCount() { + assertEquals(3, state.getChildNodeCount()); + } + + @Test + public void testGetChildNode() { + assertNotNull(state.getChildNode("x")); + assertNotNull(state.getChildNode("y")); + assertNotNull(state.getChildNode("z")); + assertNull(state.getChildNode("a")); + } + + @Test + public void testGetChildNodeEntries() { + List names = new ArrayList(); + for (ChildNodeEntry entry : state.getChildNodeEntries()) { + names.add(entry.getName()); + } + Collections.sort(names); + assertEquals(Arrays.asList("x", "y", "z"), names); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/kernel/KernelNodeStoreTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/kernel/KernelNodeStoreTest.java new file mode 100644 index 00000000000..352382124bc --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/kernel/KernelNodeStoreTest.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.kernel; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.spi.commit.CommitHook; +import org.apache.jackrabbit.oak.spi.commit.Observer; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStoreBranch; +import org.junit.Before; +import org.junit.Test; + +import static org.apache.jackrabbit.oak.api.Type.LONG; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class KernelNodeStoreTest { + + private KernelNodeStore store; + + private NodeState root; + + @Before + public void setUp() { + MicroKernel kernel = new MicroKernelImpl(); + String jsop = + "+\"test\":{\"a\":1,\"b\":2,\"c\":3," + + "\"x\":{},\"y\":{},\"z\":{}}"; + kernel .commit("/", jsop, null, "test data"); + store = new KernelNodeStore(kernel); + root = store.getRoot(); + } + + @Test + public void getRoot() { + assertEquals(root, store.getRoot()); + assertEquals(root.getChildNode("test"), store.getRoot().getChildNode("test")); + assertEquals(root.getChildNode("test").getChildNode("x"), + store.getRoot().getChildNode("test").getChildNode("x")); + assertEquals(root.getChildNode("test").getChildNode("any"), + store.getRoot().getChildNode("test").getChildNode("any")); + assertEquals(root.getChildNode("test").getProperty("a"), + store.getRoot().getChildNode("test").getProperty("a")); + assertEquals(root.getChildNode("test").getProperty("any"), + store.getRoot().getChildNode("test").getProperty("any")); + } + + @Test + public void branch() throws CommitFailedException { + NodeStoreBranch branch = store.branch(); + + NodeBuilder rootBuilder = branch.getRoot().builder(); + NodeBuilder testBuilder = rootBuilder.child("test"); + NodeBuilder newNodeBuilder = testBuilder.child("newNode"); + + testBuilder.removeNode("x"); + + newNodeBuilder.setProperty("n", 42); + + // Assert changes are present in the builder + NodeState testState = rootBuilder.getNodeState().getChildNode("test"); + assertNotNull(testState.getChildNode("newNode")); + assertNull(testState.getChildNode("x")); + assertEquals(42, (long) testState.getChildNode("newNode").getProperty("n").getValue(LONG)); + + // Assert changes are not yet present in the branch + testState = branch.getRoot().getChildNode("test"); + assertNull(testState.getChildNode("newNode")); + assertNotNull(testState.getChildNode("x")); + + branch.setRoot(rootBuilder.getNodeState()); + + // Assert changes are present in the branch + testState = branch.getRoot().getChildNode("test"); + assertNotNull(testState.getChildNode("newNode")); + assertNull(testState.getChildNode("x")); + assertEquals(42, (long) testState.getChildNode("newNode").getProperty("n").getValue(LONG)); + + // Assert changes are not yet present in the trunk + testState = store.getRoot().getChildNode("test"); + assertNull(testState.getChildNode("newNode")); + assertNotNull(testState.getChildNode("x")); + + branch.merge(); + + // Assert changes are present in the trunk + testState = store.getRoot().getChildNode("test"); + assertNotNull(testState.getChildNode("newNode")); + assertNull(testState.getChildNode("x")); + assertEquals(42, (long) testState.getChildNode("newNode").getProperty("n").getValue(LONG)); + } + + @Test + public void afterCommitHook() throws CommitFailedException { + final NodeState[] states = new NodeState[2]; // { before, after } + store.setObserver(new Observer() { + @Override + public void contentChanged(NodeState before, NodeState after) { + states[0] = before; + states[1] = after; + } + }); + + NodeState root = store.getRoot(); + NodeBuilder rootBuilder= root.builder(); + NodeBuilder testBuilder = rootBuilder.child("test"); + NodeBuilder newNodeBuilder = testBuilder.child("newNode"); + + newNodeBuilder.setProperty("n", 42); + + testBuilder.removeNode("a"); + + NodeState newRoot = rootBuilder.getNodeState(); + + NodeStoreBranch branch = store.branch(); + branch.setRoot(newRoot); + branch.merge(); + store.getRoot(); // triggers the observer + + NodeState before = states[0]; + NodeState after = states[1]; + assertNotNull(before); + assertNotNull(after); + + assertNull(before.getChildNode("test").getChildNode("newNode")); + assertNotNull(after.getChildNode("test").getChildNode("newNode")); + assertNull(after.getChildNode("test").getChildNode("a")); + assertEquals(42, (long) after.getChildNode("test").getChildNode("newNode").getProperty("n").getValue(LONG)); + assertEquals(newRoot, after); + } + + @Test + public void beforeCommitHook() throws CommitFailedException { + store.setHook(new CommitHook() { + @Override + public NodeState processCommit(NodeState before, NodeState after) { + NodeBuilder rootBuilder = after.builder(); + NodeBuilder testBuilder = rootBuilder.child("test"); + testBuilder.child("fromHook"); + return rootBuilder.getNodeState(); + } + }); + + NodeState root = store.getRoot(); + NodeBuilder rootBuilder = root.builder(); + NodeBuilder testBuilder = rootBuilder.child("test"); + NodeBuilder newNodeBuilder = testBuilder.child("newNode"); + + newNodeBuilder.setProperty("n", 42); + + testBuilder.removeNode("a"); + + NodeState newRoot = rootBuilder.getNodeState(); + + NodeStoreBranch branch = store.branch(); + branch.setRoot(newRoot); + branch.merge(); + + NodeState test = store.getRoot().getChildNode("test"); + assertNotNull(test.getChildNode("newNode")); + assertNotNull(test.getChildNode("fromHook")); + assertNull(test.getChildNode("a")); + assertEquals(42, (long) test.getChildNode("newNode").getProperty("n").getValue(LONG)); + assertEquals(test, store.getRoot().getChildNode("test")); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/kernel/LargeKernelNodeStateTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/kernel/LargeKernelNodeStateTest.java new file mode 100644 index 00000000000..54e48e8a52e --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/kernel/LargeKernelNodeStateTest.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.kernel; + +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStore; +import org.apache.jackrabbit.oak.spi.state.NodeStoreBranch; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; + +public class LargeKernelNodeStateTest { + + private static final int N = KernelNodeState.MAX_CHILD_NODE_NAMES; + + private NodeState state; + + @Before + public void setUp() throws CommitFailedException { + NodeStore store = new KernelNodeStore(new MicroKernelImpl()); + NodeStoreBranch branch = store.branch(); + + NodeBuilder builder = branch.getRoot().builder(); + builder.setProperty("a", 1); + for (int i = 0; i <= N; i++) { + builder.child("x" + i); + } + branch.setRoot(builder.getNodeState()); + + state = branch.merge(); + } + + @After + public void tearDown() { + state = null; + } + + @Test + public void testGetChildNodeCount() { + assertEquals(N + 1, state.getChildNodeCount()); + } + + @Test + public void testGetChildNode() { + assertNotNull(state.getChildNode("x0")); + assertNotNull(state.getChildNode("x1")); + assertNotNull(state.getChildNode("x" + N)); + assertNull(state.getChildNode("x" + (N + 1))); + } + + @Test + @SuppressWarnings("unused") + public void testGetChildNodeEntries() { + long count = 0; + for (ChildNodeEntry entry : state.getChildNodeEntries()) { + count++; + } + assertEquals(N + 1, count); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/namepath/NamePathMapperImplTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/namepath/NamePathMapperImplTest.java new file mode 100644 index 00000000000..c22ea1a83e5 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/namepath/NamePathMapperImplTest.java @@ -0,0 +1,205 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.namepath; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +public class NamePathMapperImplTest { + private TestNameMapper mapper = new TestNameMapper(true); + private NamePathMapper npMapper = new NamePathMapperImpl(mapper); + + @Test + public void testInvalidIdentifierPath() { + String uuid = IdentifierManager.generateUUID(); + List invalid = new ArrayList(); + invalid.add('[' + uuid + "]abc"); + invalid.add('[' + uuid + "]/a/b/c"); + + for (String jcrPath : invalid) { + assertNull(npMapper.getOakPath(jcrPath)); + } + } + + @Test + public void testNullName() { + assertNull(npMapper.getJcrName(null)); + assertNull(npMapper.getOakName(null)); + } + + @Test + public void testEmptyName() { + assertEquals("", npMapper.getJcrName("")); + assertEquals("", npMapper.getOakName("")); + } + + @Test + public void testTrailingSlash() { + assertEquals("/oak-foo:bar/oak-quu:qux",npMapper.getOakPath("/foo:bar/quu:qux/")); + assertEquals("/a/b/c",npMapper.getOakPath("/a/b/c/")); + } + + @Test + public void testJcrToOak() { + assertEquals("/", npMapper.getOakPath("/")); + assertEquals("foo", npMapper.getOakPath("{}foo")); + assertEquals("/oak-foo:bar", npMapper.getOakPath("/foo:bar")); + assertEquals("/oak-foo:bar/oak-quu:qux", npMapper.getOakPath("/foo:bar/quu:qux")); + assertEquals("oak-foo:bar", npMapper.getOakPath("foo:bar")); + assertEquals("oak-nt:unstructured", npMapper.getOakPath("{http://www.jcp.org/jcr/nt/1.0}unstructured")); + assertEquals("foobar/oak-jcr:content", npMapper.getOakPath("foobar/{http://www.jcp.org/jcr/1.0}content")); + assertEquals("foobar", npMapper.getOakPath("foobar/{http://www.jcp.org/jcr/1.0}content/..")); + assertEquals("", npMapper.getOakPath("foobar/{http://www.jcp.org/jcr/1.0}content/../..")); + assertEquals("..", npMapper.getOakPath("foobar/{http://www.jcp.org/jcr/1.0}content/../../..")); + assertEquals("../..", npMapper.getOakPath("foobar/{http://www.jcp.org/jcr/1.0}content/../../../..")); + assertEquals("oak-jcr:content", npMapper.getOakPath("foobar/../{http://www.jcp.org/jcr/1.0}content")); + assertEquals("../oak-jcr:content", npMapper.getOakPath("foobar/../../{http://www.jcp.org/jcr/1.0}content")); + assertEquals("..", npMapper.getOakPath("..")); + assertEquals("", npMapper.getOakPath(".")); + assertEquals("foobar/oak-jcr:content", npMapper.getOakPath("foobar/{http://www.jcp.org/jcr/1.0}content/.")); + assertEquals("foobar/oak-jcr:content", npMapper.getOakPath("foobar/{http://www.jcp.org/jcr/1.0}content/./.")); + assertEquals("foobar/oak-jcr:content", npMapper.getOakPath("foobar/./{http://www.jcp.org/jcr/1.0}content")); + assertEquals("oak-jcr:content", npMapper.getOakPath("foobar/./../{http://www.jcp.org/jcr/1.0}content")); + assertEquals("/a/b/c", npMapper.getOakPath("/a/b[1]/c[01]")); + } + + @Test + public void testJcrToOakKeepIndex() { + assertEquals("/", npMapper.getOakPathKeepIndex("/")); + assertEquals("foo", npMapper.getOakPathKeepIndex("{}foo")); + assertEquals("/oak-foo:bar", npMapper.getOakPathKeepIndex("/foo:bar")); + assertEquals("/oak-foo:bar/oak-quu:qux", npMapper.getOakPathKeepIndex("/foo:bar/quu:qux")); + assertEquals("oak-foo:bar", npMapper.getOakPathKeepIndex("foo:bar")); + assertEquals("oak-nt:unstructured", npMapper.getOakPathKeepIndex("{http://www.jcp.org/jcr/nt/1.0}unstructured")); + assertEquals("foobar/oak-jcr:content", npMapper.getOakPathKeepIndex("foobar/{http://www.jcp.org/jcr/1.0}content")); + assertEquals("foobar", npMapper.getOakPathKeepIndex("foobar/{http://www.jcp.org/jcr/1.0}content/..")); + assertEquals("", npMapper.getOakPathKeepIndex("foobar/{http://www.jcp.org/jcr/1.0}content/../..")); + assertEquals("..", npMapper.getOakPathKeepIndex("foobar/{http://www.jcp.org/jcr/1.0}content/../../..")); + assertEquals("../..", npMapper.getOakPathKeepIndex("foobar/{http://www.jcp.org/jcr/1.0}content/../../../..")); + assertEquals("oak-jcr:content", npMapper.getOakPathKeepIndex("foobar/../{http://www.jcp.org/jcr/1.0}content")); + assertEquals("../oak-jcr:content", npMapper.getOakPathKeepIndex("foobar/../../{http://www.jcp.org/jcr/1.0}content")); + assertEquals("..", npMapper.getOakPathKeepIndex("..")); + assertEquals("", npMapper.getOakPathKeepIndex(".")); + assertEquals("foobar/oak-jcr:content", npMapper.getOakPathKeepIndex("foobar/{http://www.jcp.org/jcr/1.0}content/.")); + assertEquals("foobar/oak-jcr:content", npMapper.getOakPathKeepIndex("foobar/{http://www.jcp.org/jcr/1.0}content/./.")); + assertEquals("foobar/oak-jcr:content", npMapper.getOakPathKeepIndex("foobar/./{http://www.jcp.org/jcr/1.0}content")); + assertEquals("oak-jcr:content", npMapper.getOakPathKeepIndex("foobar/./../{http://www.jcp.org/jcr/1.0}content")); + assertEquals("/a/b[1]/c[1]", npMapper.getOakPathKeepIndex("/a/b[1]/c[01]")); + } + + @Test + public void testJcrToOakKeepIndexNoRemap() { + TestNameMapper mapper = new TestNameMapper(false); // a mapper with no prefix remappings present + NamePathMapper npMapper = new NamePathMapperImpl(mapper); + + checkIdentical(npMapper, "/"); + checkIdentical(npMapper, "/foo:bar"); + checkIdentical(npMapper, "/foo:bar/quu:qux"); + checkIdentical(npMapper, "foo:bar"); + } + + @Test + public void testOakToJcr() { + assertEquals("/jcr-foo:bar", npMapper.getJcrPath("/foo:bar")); + assertEquals("/jcr-foo:bar/jcr-quu:qux", npMapper.getJcrPath("/foo:bar/quu:qux")); + assertEquals("jcr-foo:bar", npMapper.getJcrPath("foo:bar")); + assertEquals(".", npMapper.getJcrPath("")); + + try { + npMapper.getJcrPath("{http://www.jcp.org/jcr/nt/1.0}unstructured"); + fail("expanded name should not be accepted"); + } catch (IllegalStateException expected) { + } + + try { + npMapper.getJcrPath("foobar/{http://www.jcp.org/jcr/1.0}content"); + fail("expanded name should not be accepted"); + } catch (IllegalStateException expected) { + } + } + + private void checkEquals(NamePathMapper npMapper, String jcrPath) { + String oakPath = npMapper.getOakPathKeepIndex(jcrPath); + assertEquals(jcrPath, oakPath); + } + + private void checkIdentical(NamePathMapper npMapper, String jcrPath) { + String oakPath = npMapper.getOakPathKeepIndex(jcrPath); + checkIdentical(jcrPath, oakPath); + } + + private static void checkIdentical(String expected, String actual) { + assertEquals(expected, actual); + if (expected != actual) { + fail("Expected the strings to be the same"); + } + } + + private class TestNameMapper extends AbstractNameMapper { + + private boolean withRemappings; + private Map uri2oakprefix = new HashMap(); + + public TestNameMapper(boolean withRemappings) { + this.withRemappings = withRemappings; + + uri2oakprefix.put("", ""); + uri2oakprefix.put("http://www.jcp.org/jcr/1.0", "jcr"); + uri2oakprefix.put("http://www.jcp.org/jcr/nt/1.0", "nt"); + uri2oakprefix.put("http://www.jcp.org/jcr/mix/1.0", "mix"); + uri2oakprefix.put("http://www.w3.org/XML/1998/namespace", "xml"); + } + + @Override + protected String getJcrPrefix(String oakPrefix) { + if (oakPrefix.isEmpty() || !withRemappings) { + return oakPrefix; + } else { + return "jcr-" + oakPrefix; + } + } + + @Override + protected String getOakPrefix(String jcrPrefix) { + if (jcrPrefix.isEmpty() || !withRemappings) { + return jcrPrefix; + } else { + return "oak-" + jcrPrefix; + } + } + + @Override + protected String getOakPrefixFromURI(String uri) { + return (withRemappings ? "oak-" : "") + uri2oakprefix.get(uri); + } + + @Override + public boolean hasSessionLocalMappings() { + return withRemappings; + } + + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/IndexHookManagerTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/IndexHookManagerTest.java new file mode 100644 index 00000000000..3c6134a26b3 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/IndexHookManagerTest.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index; + +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NODE_TYPE; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.index.IndexHookManager.IndexDefDiff; +import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeState; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.junit.Test; + +import com.google.common.collect.Lists; + +public class IndexHookManagerTest { + + @Test + public void test() throws Exception { + NodeState root = MemoryNodeState.EMPTY_NODE; + + NodeBuilder builder = root.builder(); + // this index is on the current update branch, it should be seen by the + // diff + builder.child("oak:index") + .child("existing") + .setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, + Type.NAME); + // this index is NOT the current update branch, it should NOT be seen by + // the diff + builder.child("newchild") + .child("other") + .child("oak:index") + .child("existing2") + .setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, + Type.NAME); + + NodeState before = builder.getNodeState(); + // Add index definition + builder.child("oak:index") + .child("foo") + .setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, + Type.NAME); + builder.child("test") + .child("other") + .child("oak:index") + .child("index2") + .setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, + Type.NAME); + NodeState after = builder.getNodeState(); + + // , + Map defs = new HashMap(); + IndexDefDiff diff = new IndexDefDiff(builder, defs); + after.compareAgainstBaseState(before, diff); + + List reindex = Lists.newArrayList("/oak:index/foo", + "/test/other/oak:index/index2"); + List updates = Lists.newArrayList("/oak:index/existing"); + + Iterator iterator = defs.keySet().iterator(); + while (iterator.hasNext()) { + String path = iterator.next(); + if (IndexHookManager.getAndResetReindex(defs.get(path))) { + assertTrue("Missing " + path + " from reindex list", + reindex.remove(path)); + } else { + assertTrue("Missing " + path + " from updates list", + updates.remove(path)); + } + iterator.remove(); + } + assertTrue(reindex.isEmpty()); + assertTrue(updates.isEmpty()); + assertTrue(defs.isEmpty()); + } + + @Test + public void testReindexFlag() throws Exception { + NodeState root = MemoryNodeState.EMPTY_NODE; + + NodeBuilder builder = root.builder(); + builder.child("oak:index") + .child("reindexed") + .setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, + Type.NAME) + .setProperty(IndexConstants.REINDEX_PROPERTY_NAME, true); + NodeState state = builder.getNodeState(); + + // , + Map defs = new HashMap(); + IndexDefDiff diff = new IndexDefDiff(builder, defs); + state.compareAgainstBaseState(state, diff); + + List reindex = Lists.newArrayList("/oak:index/reindexed"); + Iterator iterator = defs.keySet().iterator(); + while (iterator.hasNext()) { + String path = iterator.next(); + assertTrue("Missing " + path + " from reindex list", + reindex.remove(path)); + iterator.remove(); + } + assertTrue(reindex.isEmpty()); + assertTrue(defs.isEmpty()); + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexQueryTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexQueryTest.java new file mode 100644 index 00000000000..3bbab71aa6a --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexQueryTest.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.lucene; + +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.plugins.index.IndexHookManager; +import org.apache.jackrabbit.oak.plugins.nodetype.InitialContent; +import org.apache.jackrabbit.oak.query.AbstractQueryTest; + +/** + * Tests the query engine using the default index implementation: the + * {@link LuceneIndexProvider} + */ +public class LuceneIndexQueryTest extends AbstractQueryTest { + + @Override + protected void createTestIndexNode() throws Exception { + Tree index = root.getTree("/"); + createTestIndexNode(index, LuceneIndexConstants.TYPE_LUCENE); + root.commit(); + } + + @Override + protected ContentRepository createRepository() { + return new Oak() + .with(new InitialContent()) + .with(new LuceneIndexProvider()) + .with(new IndexHookManager(new LuceneIndexHookProvider())) + .createContentRepository(); + } + +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexTest.java new file mode 100644 index 00000000000..12eba1911fe --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexTest.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.lucene; + +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import org.apache.jackrabbit.oak.plugins.index.IndexDefinition; +import org.apache.jackrabbit.oak.plugins.index.IndexDefinitionImpl; +import org.apache.jackrabbit.oak.plugins.index.IndexHook; +import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeState; +import org.apache.jackrabbit.oak.query.ast.Operator; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.Cursor; +import org.apache.jackrabbit.oak.spi.query.Filter; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; +import org.apache.jackrabbit.oak.spi.query.QueryIndex; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.junit.Test; + +public class LuceneIndexTest implements LuceneIndexConstants { + + @Test + public void testLucene() throws Exception { + NodeState root = MemoryNodeState.EMPTY_NODE; + + NodeBuilder builder = root.builder(); + builder.child("oak:index").child("lucene") + .setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE) + .setProperty("type", TYPE_LUCENE); + + NodeState before = builder.getNodeState(); + builder.setProperty("foo", "bar"); + NodeState after = builder.getNodeState(); + + IndexHook l = new LuceneHook(builder); + after.compareAgainstBaseState(before, l.preProcess()); + l.postProcess(); + l.close(); + + IndexDefinition testDef = new IndexDefinitionImpl("lucene", + TYPE_LUCENE, "/oak:index/lucene"); + QueryIndex queryIndex = new LuceneIndex(testDef); + FilterImpl filter = new FilterImpl(null); + filter.restrictPath("/", Filter.PathRestriction.EXACT); + filter.restrictProperty("foo", Operator.EQUAL, + PropertyValues.newString("bar")); + Cursor cursor = queryIndex.query(filter, builder.getNodeState()); + assertTrue(cursor.next()); + assertEquals("/", cursor.currentRow().getPath()); + assertFalse(cursor.next()); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/index/IndexTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/IndexTest.java similarity index 83% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/index/IndexTest.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/IndexTest.java index 794300c68ef..ae75e29e600 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/index/IndexTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/IndexTest.java @@ -14,32 +14,34 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.index; +package org.apache.jackrabbit.oak.plugins.index.old; import java.util.Iterator; import java.util.Random; import java.util.TreeMap; import junit.framework.Assert; -import org.apache.jackrabbit.mk.MultiMkTestBase; import org.apache.jackrabbit.mk.api.MicroKernel; -import org.apache.jackrabbit.mk.json.JsopTest; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; /** * Tests the indexing mechanism. */ -@RunWith(Parameterized.class) -public class IndexTest extends MultiMkTestBase { +public class IndexTest { - public IndexTest(String url) { - super(url); + private MicroKernel mk; + private Indexer indexer; + + @Before + public void before() { + mk = new MicroKernelImpl(); + indexer = new Indexer(mk); + indexer.init(); } @Test public void createIndexAfterAddingData() { - Indexer indexer = new Indexer(mk); PropertyIndex indexOld = indexer.createPropertyIndex("x", false); mk.commit("/", "+ \"test\": { \"test2\": { \"id\": 1 }, \"id\": 1 }", mk.getHeadRevision(), ""); mk.commit("/", "+ \"test3\": { \"test2\": { \"id\": 2 }, \"id\": 2 }", mk.getHeadRevision(), ""); @@ -55,7 +57,6 @@ public void createIndexAfterAddingData() { @Test public void nonUnique() { - Indexer indexer = new Indexer(mk); PropertyIndex index = indexer.createPropertyIndex("id", false); mk.commit("/", "+ \"test\": { \"test2\": { \"id\": 1 }, \"id\": 1 }", mk.getHeadRevision(), ""); mk.commit("/", "+ \"test3\": { \"test2\": { \"id\": 2 }, \"id\": 2 }", mk.getHeadRevision(), ""); @@ -69,7 +70,6 @@ public void nonUnique() { @Test public void nestedAddNode() { - Indexer indexer = new Indexer(mk); PropertyIndex index = indexer.createPropertyIndex("id", true); mk.commit("/", "+ \"test\": { \"test2\": { \"id\": 2 }, \"id\": 1 }", mk.getHeadRevision(), ""); @@ -79,7 +79,6 @@ public void nestedAddNode() { @Test public void move() { - Indexer indexer = new Indexer(mk); PropertyIndex index = indexer.createPropertyIndex("id", true); mk.commit("/", "+ \"test\": { \"test2\": { \"id\": 2 }, \"id\": 1 }", mk.getHeadRevision(), ""); @@ -91,9 +90,31 @@ public void move() { Assert.assertEquals("/moved/test2", index.getPath("2", mk.getHeadRevision())); } + @Test + public void copy() { + PropertyIndex index = indexer.createPropertyIndex("id", false); + + mk.commit("/", "+ \"test\": { \"test2\": { \"id\": 2 }, \"id\": 1 }", mk.getHeadRevision(), ""); + Assert.assertEquals("/test", index.getPath("1", mk.getHeadRevision())); + Assert.assertEquals("/test/test2", index.getPath("2", mk.getHeadRevision())); + + mk.commit("/", "* \"test\": \"copied\"", mk.getHeadRevision(), ""); + Iterator it = index.getPaths("1", mk.getHeadRevision()); + Assert.assertTrue(it.hasNext()); + Assert.assertEquals("/copied", it.next()); + Assert.assertTrue(it.hasNext()); + Assert.assertEquals("/test", it.next()); + Assert.assertFalse(it.hasNext()); + it = index.getPaths("2", mk.getHeadRevision()); + Assert.assertTrue(it.hasNext()); + Assert.assertEquals("/copied/test2", it.next()); + Assert.assertTrue(it.hasNext()); + Assert.assertEquals("/test/test2", it.next()); + Assert.assertFalse(it.hasNext()); + } + @Test public void ascending() { - Indexer indexer = new Indexer(mk); BTree tree = new BTree(indexer, "test", true); tree.setMinSize(2); print(mk, tree); @@ -135,7 +156,6 @@ public void duplicateKeyNonUnique() { } private void duplicateKey(boolean unique) { - Indexer indexer = new Indexer(mk); BTree tree = new BTree(indexer, "test", unique); tree.setMinSize(2); @@ -178,7 +198,6 @@ private void duplicateKey(boolean unique) { @Test public void random() { - Indexer indexer = new Indexer(mk); BTree tree = new BTree(indexer, "test", true); tree.setMinSize(2); Random r = new Random(1); @@ -225,8 +244,8 @@ static void log(String s) { static void print(MicroKernel mk, BTree tree) { String head = mk.getHeadRevision(); - String t = mk.getNodes("/index", head, 100, 0, -1, null); - log(JsopTest.format(t)); + String t = mk.getNodes(Indexer.INDEX_CONFIG_PATH, head, 100, 0, -1, null); + log(t); Cursor c = tree.findFirst("0"); StringBuilder buff = new StringBuilder(); while (c.hasNext()) { diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/index/PrefixIndexTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/PrefixIndexTest.java similarity index 82% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/index/PrefixIndexTest.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/PrefixIndexTest.java index 04200cac917..5efbed8d12c 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/index/PrefixIndexTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/PrefixIndexTest.java @@ -14,35 +14,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.index; +package org.apache.jackrabbit.oak.plugins.index.old; import java.util.Iterator; import junit.framework.Assert; -import org.apache.jackrabbit.mk.MultiMkTestBase; +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; /** * Test the prefix index. */ -@RunWith(Parameterized.class) -public class PrefixIndexTest extends MultiMkTestBase { - - public PrefixIndexTest(String url) { - super(url); - } +public class PrefixIndexTest { @Test public void test() { - Indexer indexer = new Indexer(mk, "index"); + MicroKernel mk = new MicroKernelImpl(); + Indexer indexer = new Indexer(mk); + indexer.init(); PrefixIndex index = indexer.createPrefixIndex("d:"); String head = mk.getHeadRevision(); // meta data - String meta = mk.getNodes("/index", head); - Assert.assertEquals("{\":childNodeCount\":1,\"prefix:d:\":{\":childNodeCount\":0}}", meta); + String meta = mk.getNodes(Indexer.INDEX_CONFIG_PATH, head, 1, 0, -1, null); + + Assert.assertEquals("{\":childNodeCount\":2,\"prefix@d:\":" + + "{\":childNodeCount\":1,\":data\":{}},\":data\":{\":childNodeCount\":0}}", meta); Assert.assertEquals("", getPathList(index, "d:1", head)); @@ -74,7 +72,7 @@ public void test() { Assert.assertEquals("/test7/b", getPathList(index, "d:4", head)); } - private String getPathList(PrefixIndex index, String value, String revision) { + private static String getPathList(PrefixIndex index, String value, String revision) { StringBuilder buff = new StringBuilder(); int i = 0; for (Iterator it = index.getPaths(value, revision); it.hasNext();) { diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/index/PropertyIndexTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/PropertyIndexTest.java similarity index 81% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/index/PropertyIndexTest.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/PropertyIndexTest.java index 3d32684135a..0da919380ba 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/index/PropertyIndexTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/PropertyIndexTest.java @@ -14,34 +14,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.index; +package org.apache.jackrabbit.oak.plugins.index.old; import junit.framework.Assert; -import org.apache.jackrabbit.mk.MultiMkTestBase; +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; /** * Test the property index. */ -@RunWith(Parameterized.class) -public class PropertyIndexTest extends MultiMkTestBase { - - public PropertyIndexTest(String url) { - super(url); - } +public class PropertyIndexTest { @Test public void test() { - Indexer indexer = new Indexer(mk, "index"); + MicroKernel mk = new MicroKernelImpl(); + Indexer indexer = new Indexer(mk); + indexer.init(); PropertyIndex index = indexer.createPropertyIndex("id", true); String head = mk.getHeadRevision(); // meta data - String meta = mk.getNodes("/index", head); - Assert.assertEquals("{\":childNodeCount\":1,\"id:id\":{\":childNodeCount\":0}}", meta); + String meta = mk.getNodes(Indexer.INDEX_CONFIG_PATH, head, 1, 0, -1, null); + Assert.assertEquals("{\":childNodeCount\":2,\":data\":{\":childNodeCount\":0}," + + "\"property@id,unique\":{\":childNodeCount\":1,\":data\":{}}}", meta); String oldHead = head; @@ -64,9 +61,9 @@ public void test() { Assert.assertEquals("/test/test", index.getPath("3", head)); - reconnect(); - + // Recreate the indexer indexer = new Indexer(mk); + indexer.init(); index = indexer.createPropertyIndex("id", true); head = mk.getHeadRevision(); Assert.assertEquals("/test/test", index.getPath("3", head)); diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/QueryTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/QueryTest.java new file mode 100644 index 00000000000..366cde852e8 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/QueryTest.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.plugins.index.old; + +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME; + +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.plugins.index.old.mk.IndexWrapper; +import org.apache.jackrabbit.oak.plugins.nodetype.InitialContent; +import org.apache.jackrabbit.oak.query.AbstractQueryTest; +import org.apache.jackrabbit.oak.spi.commit.CommitHook; +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.junit.Ignore; +import org.junit.Test; + +/** + * Test the query feature. + */ +public class QueryTest extends AbstractQueryTest { + + @Override + protected ContentRepository createRepository() { + // the property and prefix index currently require the index wrapper + IndexWrapper mk = new IndexWrapper(new MicroKernelImpl(), + "/" + INDEX_DEFINITIONS_NAME + "/indexes"); + + PropertyIndexer indexer = new PropertyIndexer(mk.getIndexer()); + + return new Oak(mk) + .with(new InitialContent()) + .with((QueryIndexProvider) indexer) + .with((CommitHook) indexer) + .createContentRepository(); + } + + @Test + public void sql2Explain() throws Exception { + test("sql2_explain.txt"); + } + + @Test + @Ignore("OAK-288 prevents the index from seeing updates that happened directly on the mk") + public void sql2() throws Exception { + test("sql2.txt"); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/ConflictingMoveTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/ConflictingMoveTest.java similarity index 85% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/ConflictingMoveTest.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/ConflictingMoveTest.java index d34dcff322e..e8cf3be5150 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/ConflictingMoveTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/ConflictingMoveTest.java @@ -14,14 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk; +package org.apache.jackrabbit.oak.plugins.index.old.mk; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import org.apache.jackrabbit.mk.api.MicroKernelException; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -36,11 +35,6 @@ public ConflictingMoveTest(String url) { super(url); } - @Before - public void setUp() throws Exception { - super.setUp(); - } - @Test public void collidingMove() { String head = mk.getHeadRevision(); @@ -93,16 +87,4 @@ public void conflictingAddDelete() { } } - @Test - public void doubleDelete() { - if (!isSimpleKernel(mk)) { - // TODO - return; - } - String head = mk.getHeadRevision(); - head = mk.commit("/", "+\"a\": {}", head, ""); - mk.commit("/", "-\"a\"", head, ""); - mk.commit("/", "-\"a\"", head, ""); - } - } diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/EverythingIT.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/EverythingIT.java new file mode 100644 index 00000000000..bf05d52bf1b --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/EverythingIT.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.old.mk; + +import org.apache.jackrabbit.mk.test.MicroKernelTestSuite; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ MicroKernelTestSuite.class }) +public class EverythingIT { +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/LargeObjectTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/LargeObjectTest.java similarity index 97% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/LargeObjectTest.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/LargeObjectTest.java index 569817ec7d9..dcf9918677f 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/LargeObjectTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/LargeObjectTest.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk; +package org.apache.jackrabbit.oak.plugins.index.old.mk; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/MoveNodeIT.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/MoveNodeIT.java new file mode 100644 index 00000000000..5a1a5ce6977 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/MoveNodeIT.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.old.mk; + +import static org.junit.Assert.fail; +import junit.framework.Assert; +import org.apache.jackrabbit.mk.json.JsopReader; +import org.apache.jackrabbit.mk.json.JsopTokenizer; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/** + * Test moving nodes. + */ +@RunWith(Parameterized.class) +public class MoveNodeIT extends MultiMkTestBase { + + private String head; + private String journalRevision; + + public MoveNodeIT(String url) { + super(url); + } + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + head = mk.getHeadRevision(); + commit("/", "+ \"test\": {\"a\":{}, \"b\":{}, \"c\":{}}"); + commit("/", "+ \"test2\": {}"); + getJournal(); + } + + @Test + public void moveTryOverwriteExisting() { + // move /test/b to /test2 + commit("/", "> \"test/b\": \"/test2/b\""); + + try { + // try to move /test/a to /test2/b + commit("/", "> \"test/a\": \"/test2/b\""); + fail(); + } catch (Exception e) { + // expected + } + } + + @Test + public void moveTryBecomeDescendantOfSelf() { + // move /test to /test/a/test + + try { + // try to move /test to /test/a/test + commit("/", "> \"test\": \"/test/a/test\""); + fail(); + } catch (Exception e) { + // expected + } + } + + private void commit(String root, String diff) { + head = mk.commit(root, diff, head, null); + } + + private String getJournal() { + if (journalRevision == null) { + String revs = mk.getRevisionHistory(0, 1, null); + JsopTokenizer t = new JsopTokenizer(revs); + t.read('['); + do { + t.read('{'); + Assert.assertEquals("id", t.readString()); + t.read(':'); + journalRevision = t.readString(); + t.read(','); + Assert.assertEquals("ts", t.readString()); + t.read(':'); + t.read(JsopReader.NUMBER); + t.read(','); + Assert.assertEquals("msg", t.readString()); + t.read(':'); + t.read(); + t.read('}'); + } while (t.matches(',')); + } + String head = mk.getHeadRevision(); + String journal = mk.getJournal(journalRevision, head, null); + JsopTokenizer t = new JsopTokenizer(journal); + StringBuilder buff = new StringBuilder(); + t.read('['); + boolean isNew = false; + do { + t.read('{'); + Assert.assertEquals("id", t.readString()); + t.read(':'); + t.readString(); + t.read(','); + Assert.assertEquals("ts", t.readString()); + t.read(':'); + t.read(JsopReader.NUMBER); + t.read(','); + Assert.assertEquals("msg", t.readString()); + t.read(':'); + t.read(); + t.read(','); + Assert.assertEquals("changes", t.readString()); + t.read(':'); + String changes = t.readString().trim(); + if (isNew) { + if (buff.length() > 0) { + buff.append('\n'); + } + buff.append(changes); + } + // the first revision isn't new, all others are + isNew = true; + t.read('}'); + } while (t.matches(',')); + journalRevision = head; + return buff.toString(); + } + +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/MultiMkTestBase.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/MultiMkTestBase.java similarity index 88% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/MultiMkTestBase.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/MultiMkTestBase.java index fdd4614b8f4..c8fece0d411 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/MultiMkTestBase.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/MultiMkTestBase.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk; +package org.apache.jackrabbit.oak.plugins.index.old.mk; import java.util.ArrayList; import java.util.Arrays; @@ -22,16 +22,20 @@ import java.util.List; import org.apache.jackrabbit.mk.api.MicroKernel; -import org.apache.jackrabbit.mk.fs.FileUtils; import org.apache.jackrabbit.mk.json.JsopBuilder; import org.apache.jackrabbit.mk.json.JsopReader; import org.apache.jackrabbit.mk.json.JsopTokenizer; -import org.apache.jackrabbit.mk.simple.NodeImpl; -import org.apache.jackrabbit.mk.simple.NodeMap; +import org.apache.jackrabbit.oak.plugins.index.old.mk.MicroKernelFactory; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeImpl; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeMap; import org.junit.After; import org.junit.Before; import org.junit.runners.Parameterized.Parameters; +/** + * The base class for tests that are run using multiple MicroKernel + * implementations. + */ public class MultiMkTestBase { private static final boolean PROFILE = false; @@ -56,11 +60,10 @@ public static Collection urls() { @Before public void setUp() throws Exception { - FileUtils.deleteRecursive("target/temp", false); mk = MicroKernelFactory.getInstance(url + ";clean"); cleanRepository(mk); - String root = mk.getNodes("/", mk.getHeadRevision()); + String root = mk.getNodes("/", mk.getHeadRevision(), 1, 0, -1, null); NodeImpl rootNode = NodeImpl.parse(root); if (rootNode.getPropertyCount() > 0) { System.out.println("Last mk not disposed: " + root); @@ -81,7 +84,7 @@ public void tearDown() throws InterruptedException { if (prof != null) { System.out.println(prof.getTop(5)); } - mk.dispose(); + MicroKernelFactory.disposeInstance(mk); } protected void reconnect() { @@ -89,7 +92,7 @@ protected void reconnect() { if (url.equals("simple:")) { return; } - mk.dispose(); + MicroKernelFactory.disposeInstance(mk); } mk = MicroKernelFactory.getInstance(url); } @@ -97,6 +100,7 @@ protected void reconnect() { /** * Whether this is (directly or indirectly) the MemoryKernelImpl. * + * @param mk the MicroKernel implementation * @return true if it is */ public static boolean isSimpleKernel(MicroKernel mk) { diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/Profiler.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/Profiler.java similarity index 99% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/Profiler.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/Profiler.java index 763b1523960..413145b3a9d 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/Profiler.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/Profiler.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk; +package org.apache.jackrabbit.oak.plugins.index.old.mk; import java.util.ArrayList; import java.util.HashMap; @@ -82,6 +82,7 @@ public void stopCollecting() { } } + @Override public void run() { time = System.currentTimeMillis(); while (!stop) { diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/hash/HashTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/hash/HashTest.java similarity index 80% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/hash/HashTest.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/hash/HashTest.java index 6d07d7115f0..4865901f8f3 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/hash/HashTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/hash/HashTest.java @@ -14,14 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.hash; +package org.apache.jackrabbit.oak.plugins.index.old.mk.hash; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.util.Arrays; -import org.apache.jackrabbit.mk.MultiMkTestBase; -import org.apache.jackrabbit.mk.simple.NodeImpl; -import org.apache.jackrabbit.mk.util.IOUtilsTest; + +import org.apache.jackrabbit.oak.plugins.index.old.mk.MultiMkTestBase; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeImpl; import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,6 +39,7 @@ public HashTest(String url) { super(url); } + @Override @After public void tearDown() throws InterruptedException { if (isSimpleKernel(mk)) { @@ -60,11 +61,11 @@ public void getHash() { head = mk.commit("/", "+ \"test1\": { \"id\": 1 }", mk.getHeadRevision(), ""); head = mk.commit("/", "+ \"test2\": { \"id\": 1 }", mk.getHeadRevision(), ""); - NodeImpl r = NodeImpl.parse(mk.getNodes("/", head)); + NodeImpl r = NodeImpl.parse(mk.getNodes("/", head, 1, 0, -1, null)); assertTrue(r.getHash() != null); - NodeImpl t1 = NodeImpl.parse(mk.getNodes("/test1", head)); - NodeImpl t2 = NodeImpl.parse(mk.getNodes("/test2", head)); - IOUtilsTest.assertEquals(t1.getHash(), t2.getHash()); + NodeImpl t1 = NodeImpl.parse(mk.getNodes("/test1", head, 1, 0, -1, null)); + NodeImpl t2 = NodeImpl.parse(mk.getNodes("/test2", head, 1, 0, -1, null)); + assertTrue(Arrays.equals(t1.getHash(), t2.getHash())); assertFalse(Arrays.equals(t1.getHash(), r.getHash())); } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/json/fast/Jsop.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/json/fast/Jsop.java similarity index 98% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/json/fast/Jsop.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/json/fast/Jsop.java index 33e0dec03ab..662e1594853 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/json/fast/Jsop.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/json/fast/Jsop.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.json.fast; +package org.apache.jackrabbit.oak.plugins.index.old.mk.json.fast; import java.math.BigDecimal; import org.apache.jackrabbit.mk.json.JsopBuilder; diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/json/fast/JsopArray.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/json/fast/JsopArray.java similarity index 91% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/json/fast/JsopArray.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/json/fast/JsopArray.java index cb384b41f5a..817bae9bb26 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/json/fast/JsopArray.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/json/fast/JsopArray.java @@ -14,15 +14,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.json.fast; +package org.apache.jackrabbit.oak.plugins.index.old.mk.json.fast; + +import org.apache.jackrabbit.mk.json.JsopBuilder; +import org.apache.jackrabbit.mk.json.JsopTokenizer; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.ListIterator; -import org.apache.jackrabbit.mk.json.JsopBuilder; -import org.apache.jackrabbit.mk.json.JsopTokenizer; /** * An array of objects. @@ -43,6 +44,7 @@ public JsopArray() { list = new ArrayList(); } + @Override public Object get(int index) { init(); String s = load(index); @@ -85,17 +87,20 @@ private void init() { } } + @Override public boolean isEmpty() { init(); return list == EMPTY_LIST; } + @Override public int size() { init(); load(Integer.MAX_VALUE); return list.size(); } + @Override public String toString() { if (jsop == null) { JsopBuilder w = new JsopBuilder(); @@ -110,12 +115,14 @@ public String toString() { return jsop.substring(start); } + @Override public boolean add(Object e) { initWrite(); list.add(toString(e)); return true; } + @Override public void clear() { initWrite(); list.clear(); @@ -130,30 +137,37 @@ private void readAll() { load(Integer.MAX_VALUE); } + @Override public void add(int index, Object element) { throw new UnsupportedOperationException(); } - public boolean addAll(Collection c) { + @Override + public boolean addAll(Collection c) { throw new UnsupportedOperationException(); } - public boolean addAll(int index, Collection c) { + @Override + public boolean addAll(int index, Collection c) { throw new UnsupportedOperationException(); } + @Override public boolean contains(Object o) { throw new UnsupportedOperationException(); } + @Override public boolean containsAll(Collection c) { throw new UnsupportedOperationException(); } + @Override public int indexOf(Object o) { throw new UnsupportedOperationException(); } + @Override public Iterator iterator() { return new Iterator() { @@ -177,46 +191,57 @@ public void remove() { }; } + @Override public int lastIndexOf(Object o) { throw new UnsupportedOperationException(); } + @Override public ListIterator listIterator() { throw new UnsupportedOperationException(); } + @Override public ListIterator listIterator(int index) { throw new UnsupportedOperationException(); } + @Override public boolean remove(Object o) { throw new UnsupportedOperationException(); } + @Override public Object remove(int index) { throw new UnsupportedOperationException(); } + @Override public boolean removeAll(Collection c) { throw new UnsupportedOperationException(); } + @Override public boolean retainAll(Collection c) { throw new UnsupportedOperationException(); } + @Override public Object set(int index, Object element) { throw new UnsupportedOperationException(); } + @Override public List subList(int fromIndex, int toIndex) { throw new UnsupportedOperationException(); } + @Override public Object[] toArray() { throw new UnsupportedOperationException(); } + @Override public T[] toArray(T[] a) { throw new UnsupportedOperationException(); } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/json/fast/JsopObject.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/json/fast/JsopObject.java similarity index 95% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/json/fast/JsopObject.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/json/fast/JsopObject.java index eb8000d25e3..50360c44af3 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/json/fast/JsopObject.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/json/fast/JsopObject.java @@ -14,15 +14,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.json.fast; +package org.apache.jackrabbit.oak.plugins.index.old.mk.json.fast; + +import org.apache.jackrabbit.mk.json.JsopBuilder; +import org.apache.jackrabbit.mk.json.JsopTokenizer; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; -import org.apache.jackrabbit.mk.json.JsopBuilder; -import org.apache.jackrabbit.mk.json.JsopTokenizer; /** * A map. @@ -64,6 +65,7 @@ private void init() { } } + @Override public Object get(Object key) { init(); String v = map.get(key); @@ -107,16 +109,19 @@ public Object get(Object key) { return null; } + @Override public boolean containsKey(Object key) { get(key); return map.containsKey(key); } + @Override public boolean isEmpty() { init(); return map == EMPTY_MAP; } + @Override public int size() { readAll(); return map.size(); @@ -131,6 +136,7 @@ private void initWrite() { jsop = null; } + @Override public String toString() { if (jsop == null) { JsopBuilder w = new JsopBuilder(); @@ -158,37 +164,45 @@ public String toString() { return jsop.substring(start); } + @Override public void clear() { initWrite(); map.clear(); } + @Override public Object put(String key, Object value) { initWrite(); String old = map.put(key, toString(value)); return Jsop.parse(old); } + @Override public boolean containsValue(Object value) { throw new UnsupportedOperationException(); } + @Override public Set> entrySet() { throw new UnsupportedOperationException(); } + @Override public Set keySet() { throw new UnsupportedOperationException(); } - public void putAll(Map m) { + @Override + public void putAll(Map m) { throw new UnsupportedOperationException(); } + @Override public Object remove(Object key) { throw new UnsupportedOperationException(); } + @Override public Collection values() { throw new UnsupportedOperationException(); } diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/json/fast/JsopObjectTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/json/fast/JsopObjectTest.java similarity index 95% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/json/fast/JsopObjectTest.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/json/fast/JsopObjectTest.java index f3e55c593b0..c345fdfa8dc 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/json/fast/JsopObjectTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/json/fast/JsopObjectTest.java @@ -14,10 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.json.fast; +package org.apache.jackrabbit.oak.plugins.index.old.mk.json.fast; import java.math.BigDecimal; -import org.apache.jackrabbit.mk.util.StopWatch; + import junit.framework.TestCase; /** @@ -37,12 +37,14 @@ public static void main(String... args) { w.put("child" + j, data); } String jsop = w.toString(); - StopWatch timer = new StopWatch(); + long start = System.nanoTime(); for (int j = 0; j < 10000; j++) { JsopObject o = (JsopObject) Jsop.parse(jsop); assertEquals(data, o.get("child99")); } - System.out.println(timer.seconds() + " lengthIndex=" + lengthIndex); + double seconds = (System.nanoTime() - start) / 1.0e9; + System.out.format( + "%.2f seconds lengthIndex=%d%n", seconds, lengthIndex); } } @@ -101,7 +103,7 @@ public void testArray() { // expected } String s = ""; - for(Object o : a) { + for (Object o : a) { s += o + ";"; } assertEquals("1;null;Hello;[];{};", s); diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/large/CreateNodesTraverseTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/CreateNodesTraverseTest.java similarity index 91% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/large/CreateNodesTraverseTest.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/CreateNodesTraverseTest.java index 58e82b86702..df5ff96f7ae 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/large/CreateNodesTraverseTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/CreateNodesTraverseTest.java @@ -11,10 +11,9 @@ * KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -package org.apache.jackrabbit.mk.large; +package org.apache.jackrabbit.oak.plugins.index.old.mk.large; -import org.apache.jackrabbit.mk.MultiMkTestBase; -import org.apache.jackrabbit.mk.util.NodeCreator; +import org.apache.jackrabbit.oak.plugins.index.old.mk.MultiMkTestBase; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/CreateRandomNodesTraverseTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/CreateRandomNodesTraverseTest.java new file mode 100644 index 00000000000..25c4236edfd --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/CreateRandomNodesTraverseTest.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.plugins.index.old.mk.large; + +import org.apache.jackrabbit.oak.plugins.index.old.mk.MultiMkTestBase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/** + * Creates and then reads nodes distributed in a tree with random width at each + * level. + */ +@RunWith(Parameterized.class) +public class CreateRandomNodesTraverseTest extends MultiMkTestBase { + + public CreateRandomNodesTraverseTest(String url) { + super(url); + } + + @Test + public void test() throws Exception { + RandomNodeCreator c = new RandomNodeCreator(mk, 1); + c.setTotalCount(200); + c.setMaxWidth(10); + // c.setLogToSystemOut(true); + + // 1 million node test + // c.setLogToSystemOut(true); + // c.setTotalCount(1000000); + + // 20 million node test + // c.setTotalCount(20000000); + + c.create(); + c.traverse(); + } +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/large/DescendantCountTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/DescendantCountTest.java similarity index 89% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/large/DescendantCountTest.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/DescendantCountTest.java index 14648b176bf..3490c7a9975 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/large/DescendantCountTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/DescendantCountTest.java @@ -11,16 +11,15 @@ * KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -package org.apache.jackrabbit.mk.large; +package org.apache.jackrabbit.oak.plugins.index.old.mk.large; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; -import org.apache.jackrabbit.mk.MultiMkTestBase; import org.apache.jackrabbit.mk.json.JsopBuilder; -import org.apache.jackrabbit.mk.json.fast.Jsop; -import org.apache.jackrabbit.mk.json.fast.JsopObject; -import org.apache.jackrabbit.mk.simple.NodeImpl; -import org.apache.jackrabbit.mk.util.NodeCreator; +import org.apache.jackrabbit.oak.plugins.index.old.mk.MultiMkTestBase; +import org.apache.jackrabbit.oak.plugins.index.old.mk.json.fast.Jsop; +import org.apache.jackrabbit.oak.plugins.index.old.mk.json.fast.JsopObject; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeImpl; import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; @@ -36,6 +35,7 @@ public DescendantCountTest(String url) { super(url); } + @Override @After public void tearDown() throws InterruptedException { if (isSimpleKernel(mk)) { @@ -56,7 +56,7 @@ public void test() throws Exception { NodeCreator c = new NodeCreator(mk); for (int i = 1; i < 20; i++) { - c.setNodeName("test" + i); + c.setTestRoot("test" + i); c.setWidth(2); c.setTotalCount(i); c.setData(null); diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/large/LargeNodeTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/LargeNodeTest.java similarity index 95% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/large/LargeNodeTest.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/LargeNodeTest.java index 16dd1a2a8ff..db487c2d404 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/large/LargeNodeTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/LargeNodeTest.java @@ -14,11 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.large; +package org.apache.jackrabbit.oak.plugins.index.old.mk.large; import junit.framework.Assert; -import org.apache.jackrabbit.mk.MultiMkTestBase; -import org.apache.jackrabbit.mk.util.StopWatch; + +import org.apache.jackrabbit.oak.plugins.index.old.mk.MultiMkTestBase; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -37,6 +37,7 @@ public LargeNodeTest(String url) { super(url); } + @Override @Before public void setUp() throws Exception { super.setUp(); @@ -44,6 +45,7 @@ public void setUp() throws Exception { commit("/", "+ \"t\": {\"a\":{}, \"b\":{}, \"c\":{}}"); } + @Override @After public void tearDown() throws InterruptedException { if (isSimpleKernel(mk)) { @@ -56,7 +58,7 @@ public void tearDown() throws InterruptedException { @Test public void getNodes() { head = mk.commit("/", "+\"x0\" : {\"x\": 0, \"x1\":{\"x\":1, \"x2\": {\"x\": -3}}}", head, null); - String s = mk.getNodes("/x0", head); + String s = mk.getNodes("/x0", head, 1, 0, -1, null); Assert.assertEquals("{\"x\":0,\":childNodeCount\":1,\"x1\":{\"x\":1,\":childNodeCount\":1,\"x2\":{}}}", s); s = mk.getNodes("/x0", head, 1, 0, -1, null); Assert.assertEquals("{\"x\":0,\":childNodeCount\":1,\"x1\":{\"x\":1,\":childNodeCount\":1,\"x2\":{}}}", s); @@ -74,13 +76,13 @@ public void largeNodeListAndGetNodes() { } int max = 90; head = mk.commit("/:root/head/config", "^ \"maxMemoryChildren\":" + max, head, ""); - Assert.assertEquals("{\"maxMemoryChildren\":"+max+",\":childNodeCount\":0}", mk.getNodes("/:root/head/config", head)); + Assert.assertEquals("{\"maxMemoryChildren\":"+max+",\":childNodeCount\":0}", mk.getNodes("/:root/head/config", head, 1, 0, -1, null)); head = mk.commit("/", "+ \"test\": {}", head, ""); for (int i = 0; i < 100; i++) { head = mk.commit("/", "+ \"test/" + i + "\": {\"x\":" + i + "}\n", head, ""); } Assert.assertTrue(mk.nodeExists("/test", head)); - mk.getNodes("/test", head); + mk.getNodes("/test", head, 1, 0, -1, null); } @Test @@ -88,7 +90,7 @@ public void veryLargeNodeList() { if (isSimpleKernel(mk)) { int max = 2000; head = mk.commit("/:root/head/config", "^ \"maxMemoryChildren\":" + max, head, ""); - Assert.assertEquals("{\"maxMemoryChildren\":"+max+",\":childNodeCount\":0}", mk.getNodes("/:root/head/config", head)); + Assert.assertEquals("{\"maxMemoryChildren\":"+max+",\":childNodeCount\":0}", mk.getNodes("/:root/head/config", head, 1, 0, -1, null)); } head = mk.commit("/", "+ \"test\": {}", head, ""); @@ -125,7 +127,7 @@ public void largeNodeList() { } head = mk.commit("/:root/head/config", "^ \"maxMemoryChildren\": 10", head, ""); - Assert.assertEquals("{\"maxMemoryChildren\":10,\":childNodeCount\":0}", mk.getNodes("/:root/head/config", head)); + Assert.assertEquals("{\"maxMemoryChildren\":10,\":childNodeCount\":0}", mk.getNodes("/:root/head/config", head, 1, 0, -1, null)); for (int i = 0; i < 100; i++) { head = mk.commit("/", "+ \"t" + i + "\": {\"x\":" + i + "}", head, ""); } diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/large/ManyRevisionsTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/ManyRevisionsTest.java similarity index 85% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/large/ManyRevisionsTest.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/ManyRevisionsTest.java index fbeffb96345..596db787e8e 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/large/ManyRevisionsTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/ManyRevisionsTest.java @@ -14,16 +14,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.large; +package org.apache.jackrabbit.oak.plugins.index.old.mk.large; import static org.junit.Assert.assertEquals; import java.util.ArrayList; -import org.apache.jackrabbit.mk.MultiMkTestBase; -import org.apache.jackrabbit.mk.json.fast.Jsop; -import org.apache.jackrabbit.mk.json.fast.JsopArray; -import org.apache.jackrabbit.mk.json.fast.JsopObject; -import org.apache.jackrabbit.mk.simple.NodeImpl; -import org.apache.jackrabbit.mk.util.StopWatch; + +import org.apache.jackrabbit.oak.plugins.index.old.mk.MultiMkTestBase; +import org.apache.jackrabbit.oak.plugins.index.old.mk.json.fast.Jsop; +import org.apache.jackrabbit.oak.plugins.index.old.mk.json.fast.JsopArray; +import org.apache.jackrabbit.oak.plugins.index.old.mk.json.fast.JsopObject; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeImpl; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,6 +39,7 @@ public ManyRevisionsTest(String url) { super(url); } + @Override @Before public void setUp() throws Exception { super.setUp(); @@ -58,7 +59,7 @@ public void readRevisions() { int i = 0; String last = first; for (String rev : revs) { - String n = mk.getNodes("/test", rev); + String n = mk.getNodes("/test", rev, 1, 0, -1, null); NodeImpl node = NodeImpl.parse(n); assertEquals(i, Integer.parseInt(node.getProperty("id"))); String journal = mk.getJournal(last, rev, null); diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/NodeCreator.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/NodeCreator.java similarity index 75% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/util/NodeCreator.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/NodeCreator.java index 77f5cdbf51b..c101522eee0 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/NodeCreator.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/NodeCreator.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.util; +package org.apache.jackrabbit.oak.plugins.index.old.mk.large; import org.apache.jackrabbit.mk.api.MicroKernel; @@ -23,12 +23,16 @@ */ public class NodeCreator { + private static final String DEFAULT_TEST_ROOT = "test"; + private static final int DEFAULT_COUNT = 200; + private static final int DEFAULT_WIDTH = 30; + private final MicroKernel mk; + private String testRoot = DEFAULT_TEST_ROOT; + private int totalCount = DEFAULT_COUNT; + private int width = DEFAULT_WIDTH; private String head; - private int totalCount = 200; - private int width = 30, count; - private StopWatch timer; - private String nodeName = "test"; + private int count; private String data = "Hello World"; private boolean logToSystemOut; @@ -45,8 +49,8 @@ public void setTotalCount(int totalCount) { this.totalCount = totalCount; } - public void setNodeName(String nodeName) { - this.nodeName = nodeName; + public void setTestRoot(String testRoot) { + this.testRoot = testRoot; } public void setData(String data) { @@ -54,25 +58,21 @@ public void setData(String data) { } public void create() { - log("implementation: " + mk); - log("creating " + totalCount + " nodes"); - head = mk.commit("/", "+\"" + nodeName + "\":{}", head, ""); - timer = new StopWatch(); + log("Implementation: " + mk); + log("Creating " + totalCount + " nodes under " + testRoot); + head = mk.commit("/", "+\"" + testRoot + "\":{}", head, ""); count = 0; int depth = (int) Math.ceil(Math.log(totalCount) / Math.log(width)); - log("depth: " + depth); - createNodes(nodeName, depth); - log("created " + count + " nodes in " + timer.operationsPerSecond(count)); + log("Depth: " + depth); + createNodes(testRoot, depth); log(""); } public void traverse() { - timer = new StopWatch(); count = 0; int depth = (int) Math.ceil(Math.log(totalCount) / Math.log(width)); - log("depth: " + depth); - traverse(nodeName, depth); - log("read " + count + " nodes in " + timer.operationsPerSecond(count)); + log("Depth: " + depth); + traverse(testRoot, depth); } private void createNodes(String parent, int depth) { @@ -92,9 +92,6 @@ private void createNodes(String parent, int depth) { } count++; buff.append("}\n"); - if (count % 1000 == 0 && timer.log()) { - log(" " + count + " nodes in " + timer.operationsPerSecond(count)); - } } head = mk.commit("/", buff.toString(), head, ""); if (depth > 0) { @@ -117,11 +114,8 @@ private void traverse(String parent, int depth) { if (!mk.nodeExists("/" + p, head)) { break; } - mk.getNodes("/" + p, head); + mk.getNodes("/" + p, head, 1, 0, -1, null); count++; - if (count % 1000 == 0 && timer.log()) { - log(" " + count + " nodes in " + timer.operationsPerSecond(count)); - } if (depth > 0) { traverse(p, depth - 1); } diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/RandomNodeCreator.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/RandomNodeCreator.java new file mode 100644 index 00000000000..0cf1af6c467 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/RandomNodeCreator.java @@ -0,0 +1,150 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.old.mk.large; + +import java.util.LinkedList; +import java.util.Queue; +import java.util.Random; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.json.JsopBuilder; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeImpl; + +/** + * A utility to create a number of nodes in a random tree structure. Each level + * has a random (but fixed for the level) number of nodes with at most maxWidth + * nodes. + */ +public class RandomNodeCreator { + + private static final String DEFAULT_TEST_ROOT = "testRandom"; + private static final int DEFAULT_COUNT = 200; + private static final int DEFAULT_WIDTH = 30; + + private final MicroKernel mk; + private final Random random; + + private String testRoot = DEFAULT_TEST_ROOT; + private int totalCount = DEFAULT_COUNT; + private int maxWidth = DEFAULT_WIDTH; + private boolean logToSystemOut; + private String head; + private int count; + private Queue queue = new LinkedList(); + + public RandomNodeCreator(MicroKernel mk, int seed) { + this.mk = mk; + head = mk.getHeadRevision(); + random = new Random(seed); + } + + public void setTestRoot(String testRoot) { + this.testRoot = testRoot; + } + + public void setTotalCount(int totalCount) { + this.totalCount = totalCount; + } + + public void setMaxWidth(int maxWidth) { + this.maxWidth = maxWidth; + } + + public void setLogToSystemOut(boolean logToSystemOut) { + this.logToSystemOut = logToSystemOut; + } + + public void create() { + log("Implementation: " + mk); + log("Creating " + totalCount + " nodes under " + testRoot); + head = mk.commit("/", "+\"" + testRoot + "\":{}", head, ""); + count = 0; + createNodes(testRoot); + } + + public void traverse() { + count = 0; + queue.clear(); + log("Traversing " + totalCount + " nodes"); + traverse(testRoot); + } + + private void createNodes(String parent) { + if (count >= totalCount) { + return; + } + + int width = random.nextInt(maxWidth) + 1; + + head = mk.commit("/" + parent, "^ \"width\":" + width, head, null); + + StringBuilder buff = new StringBuilder(); + for (int i = 0; i < width; i++) { + if (count >= totalCount) { + break; + } + + String p = parent + "/node" + count; + queue.add(p); + buff.append("+ \"" + p + "\": {"); + buff.append("}\n"); + count++; + } + head = mk.commit("/", buff.toString(), head, ""); + log("Committed with width: " + width + "\n" + buff.toString()); + + while (!queue.isEmpty()) { + createNodes(queue.poll()); + } + } + + private void traverse(String parent) { + if (count >= totalCount) { + return; + } + + String parentJson = JsopBuilder.prettyPrint(mk.getNodes("/" + parent, mk.getHeadRevision(), 1, 0, -1, null)); + NodeImpl parentNode = NodeImpl.parse(parentJson); + int width = Integer.parseInt(parentNode.getProperty("width")); + + for (int i = 0; i < width; i++) { + if (count >= totalCount) { + break; + } + + String p = parent + "/node" + count; + log("Traversed: " + p); + if (!mk.nodeExists("/" + p, head)) { + break; + } + mk.getNodes("/" + p, head, 1, 0, -1, null); + queue.add(p); + count++; + } + + while (!queue.isEmpty()) { + traverse(queue.poll()); + } + } + + private void log(String s) { + if (logToSystemOut) { + System.out.println(s); + } + } + +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/StopWatch.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/StopWatch.java new file mode 100644 index 00000000000..098f5e35eaf --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/large/StopWatch.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.old.mk.large; + +/** + * A utility class to time an operation. + */ +public class StopWatch { + + private static final long NANOS_PER_SECOND = 1000 * 1000 * 1000; + + private long start = System.nanoTime(); + private long lastLog = start; + + public long time() { + return System.nanoTime() - start; + } + + public String seconds() { + double s = (double) time() / NANOS_PER_SECOND; + return String.format("%.2f seconds", s); + } + + public String operationsPerSecond(int operations) { + long t = time(); + double s = (double) t / NANOS_PER_SECOND; + if (t == 0) { + t = 1; + } + int ops = (int) (operations * NANOS_PER_SECOND / t); + return String.format("%.2f seconds (%d ops; %d op/s)", s, operations, ops); + } + + /** + * Returns true once 5 seconds. + * + * @return true once every 5 seconds + */ + public boolean log() { + long t = System.nanoTime(); + if (t - lastLog > 5 * NANOS_PER_SECOND) { + lastLog = t; + return true; + } + return false; + } + + public void reset() { + start = System.nanoTime(); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/mem/MemoryNodeTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/mem/MemoryNodeTest.java similarity index 95% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/mem/MemoryNodeTest.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/mem/MemoryNodeTest.java index 6759ad761b6..046a17ca524 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/mem/MemoryNodeTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/mem/MemoryNodeTest.java @@ -14,12 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.mem; +package org.apache.jackrabbit.oak.plugins.index.old.mk.mem; import junit.framework.Assert; -import org.apache.jackrabbit.mk.simple.NodeId; -import org.apache.jackrabbit.mk.simple.NodeImpl; -import org.apache.jackrabbit.mk.simple.NodeMap; + +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeId; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeImpl; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeMap; import org.junit.Test; /** diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/AscendingClockTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/AscendingClockTest.java similarity index 89% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/util/AscendingClockTest.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/AscendingClockTest.java index 071f0df9c3f..b96d57a1559 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/AscendingClockTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/AscendingClockTest.java @@ -14,10 +14,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.util; +package org.apache.jackrabbit.oak.plugins.index.old.mk.simple; + +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.AscendingClock; import junit.framework.TestCase; +/** + * A test for the class {@code AscendingClock}. + */ public class AscendingClockTest extends TestCase { public void testMillis() throws InterruptedException { diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/MoveNodeTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/MoveNodeIT.java similarity index 79% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/MoveNodeTest.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/MoveNodeIT.java index d3f916b7abc..2b0d250a08b 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/MoveNodeTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/MoveNodeIT.java @@ -14,46 +14,42 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk; +package org.apache.jackrabbit.oak.plugins.index.old.mk.simple; -import static org.junit.Assert.fail; import junit.framework.Assert; + +import org.apache.jackrabbit.mk.api.MicroKernel; import org.apache.jackrabbit.mk.api.MicroKernelException; +import org.apache.jackrabbit.mk.json.JsopReader; import org.apache.jackrabbit.mk.json.JsopTokenizer; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.SimpleKernelImpl; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; /** - * Test moving nodes. + * Test moving nodes. These tests currently only work against the + * {@link SimpleKernelImpl} implementation as they assume a specific + * ordering of child nodes that isn't guaranteed by the + * {@link MicroKernel} contract. */ -@RunWith(Parameterized.class) -public class MoveNodeTest extends MultiMkTestBase { +public class MoveNodeIT { + + private final MicroKernel mk = new SimpleKernelImpl("mem:SimpleKernelTest"); private String head; private String journalRevision; - public MoveNodeTest(String url) { - super(url); - } - @Before public void setUp() throws Exception { - super.setUp(); head = mk.getHeadRevision(); commit("/", "+ \"test\": {\"a\":{}, \"b\":{}, \"c\":{}}"); commit("/", "+ \"test2\": {}"); getJournal(); } + // TODO fix test since it incorrectly expects a specific order of child nodes @Test public void addProperty() { - if (!isSimpleKernel(mk)) { - // TODO fix test since it incorrectly expects a specific order of child nodes - return; - } - // add a property /test/c commit("/", "+ \"test/c\": 123"); Assert.assertEquals("{c:123,a,b,c}", getNode("/test")); @@ -62,10 +58,6 @@ public void addProperty() { @Test public void addPropertyTwice() { - if (!isSimpleKernel(mk)) { - return; - } - commit("/", "+ \"test/c\": 123"); // duplicate add property can fail @@ -79,23 +71,15 @@ public void addPropertyTwice() { Assert.assertEquals("{c:123,a,b,c}", getNode("/test")); } + // TODO fix test since it incorrectly expects a specific order of child nodes @Test public void order() { - if (!isSimpleKernel(mk)) { - // TODO fix test since it incorrectly expects a specific order of child nodes - return; - } - Assert.assertEquals("{a,b,c}", getNode("/test")); } + // TODO fix test since it incorrectly expects a specific order of child nodes @Test public void rename() { - if (!isSimpleKernel(mk)) { - // TODO fix test since it incorrectly expects a specific order of child nodes - return; - } - // rename /test/b commit("/", "> \"test/b\": \"test/b1\""); Assert.assertEquals("{a,b1,c}", getNode("/test")); @@ -109,10 +93,6 @@ public void rename() { @Test public void reorderBefore() { - if (!isSimpleKernel(mk)) { - return; - } - // order c before b commit("/", "> \"test/c\": {\"before\": \"test/b\"}"); Assert.assertEquals("{a,c,b}", getNode("/test")); @@ -126,10 +106,6 @@ public void reorderBefore() { @Test public void reorderAfter() { - if (!isSimpleKernel(mk)) { - return; - } - // order a after b commit("/", "> \"test/a\": {\"after\": \"test/b\"}"); Assert.assertEquals("{b,a,c}", getNode("/test")); @@ -148,10 +124,6 @@ public void reorderAfter() { @Test public void moveFirst() { - if (!isSimpleKernel(mk)) { - return; - } - // move /test/a to /test2/a (rename is not supported in this way) commit("/", "> \"test/a\": {\"first\": \"test2\"}"); Assert.assertEquals("{b,c}", getNode("/test")); @@ -167,10 +139,6 @@ public void moveFirst() { @Test public void moveCombinedWithSet() { - if (!isSimpleKernel(mk)) { - return; - } - // move /test/b to /test_b commit("/", "> \"test/b\": \"test_b\""); Assert.assertEquals("{a,c}", getNode("/test")); @@ -188,10 +156,6 @@ public void moveCombinedWithSet() { @Test public void moveBefore() { - if (!isSimpleKernel(mk)) { - return; - } - // move /test/b to /test2/b, before any other nodes in /test2 commit("/", "> \"test/b\": {\"first\": \"test2\"}"); Assert.assertEquals("{a,c}", getNode("/test")); @@ -207,10 +171,6 @@ public void moveBefore() { @Test public void moveAfter() { - if (!isSimpleKernel(mk)) { - return; - } - // move /test/c to /test2 commit("/", "> \"test/c\": \"test2/c\""); Assert.assertEquals("{a,b}", getNode("/test")); @@ -232,10 +192,6 @@ public void moveAfter() { @Test public void moveLast() { - if (!isSimpleKernel(mk)) { - return; - } - // move /test/a to /test2, as last commit("/", "> \"test/b\": {\"last\": \"test2\"}"); Assert.assertEquals("{a,c}", getNode("/test")); @@ -249,32 +205,24 @@ public void moveLast() { assertJournal(">\"/test/c\":{\"last\":\"/test2\"}"); } + // TODO fix test since it incorrectly expects a specific order of child nodes @Test public void copy() { - if (!isSimpleKernel(mk)) { - // TODO fix test since it incorrectly expects a specific order of child nodes - return; - } - // copy /test to /test2/copy commit("/", "* \"test\": \"/test2/copy\""); Assert.assertEquals("{a,b,c}", getNode("/test")); Assert.assertEquals("{copy:{a,b,c}}", getNode("/test2")); - if (isSimpleKernel(mk)) { + // if (isSimpleKernel(mk)) { assertJournal("*\"/test\":\"/test2/copy\""); - } else { - assertJournal("+\"/test2/copy\":{\"a\":{},\"b\":{},\"c\":{}}"); - } + // } else { + // assertJournal("+\"/test2/copy\":{\"a\":{},\"b\":{},\"c\":{}}"); + // } } + // TODO fix test since it incorrectly expects a specific order of child nodes @Test public void move() { - if (!isSimpleKernel(mk)) { - // TODO fix test since it incorrectly expects a specific order of child nodes - return; - } - // move /test/b to /test2 commit("/", "> \"test/b\": \"/test2/b\""); Assert.assertEquals("{a,c}", getNode("/test")); @@ -294,39 +242,12 @@ public void move() { assertJournal(">\"/test/c\":\"/test2/c\""); } - @Test - public void moveTryOverwriteExisting() { - // move /test/b to /test2 - commit("/", "> \"test/b\": \"/test2/b\""); - - try { - // try to move /test/a to /test2/b - commit("/", "> \"test/a\": \"/test2/b\""); - fail(); - } catch (Exception e) { - // expected - } - } - - @Test - public void moveTryBecomeDescendantOfSelf() { - // move /test to /test/a/test - - try { - // try to move /test to /test/a/test - commit("/", "> \"test\": \"/test/a/test\""); - fail(); - } catch (Exception e) { - // expected - } - } - private void commit(String root, String diff) { head = mk.commit(root, diff, head, null); } private String getNode(String node) { - String s = mk.getNodes(node, mk.getHeadRevision()); + String s = mk.getNodes(node, mk.getHeadRevision(), 1, 0, -1, null); s = s.replaceAll("\"", "").replaceAll(":childNodeCount:.", ""); s = s.replaceAll("\\{\\,", "\\{").replaceAll("\\,\\}", "\\}"); s = s.replaceAll("\\:\\{\\}", ""); @@ -340,7 +261,7 @@ private void assertJournal(String expectedJournal) { private String getJournal() { if (journalRevision == null) { - String revs = mk.getRevisions(0, 1); + String revs = mk.getRevisionHistory(0, 1, null); JsopTokenizer t = new JsopTokenizer(revs); t.read('['); do { @@ -351,7 +272,11 @@ private String getJournal() { t.read(','); Assert.assertEquals("ts", t.readString()); t.read(':'); - t.read(JsopTokenizer.NUMBER); + t.read(JsopReader.NUMBER); + t.read(','); + Assert.assertEquals("msg", t.readString()); + t.read(':'); + t.read(); t.read('}'); } while (t.matches(',')); } @@ -369,7 +294,7 @@ private String getJournal() { t.read(','); Assert.assertEquals("ts", t.readString()); t.read(':'); - t.read(JsopTokenizer.NUMBER); + t.read(JsopReader.NUMBER); t.read(','); Assert.assertEquals("msg", t.readString()); t.read(':'); diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/large/NodeVersionTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeVersionTest.java similarity index 63% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/large/NodeVersionTest.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeVersionTest.java index 8b3159bf295..e510abb286e 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/large/NodeVersionTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/NodeVersionTest.java @@ -14,56 +14,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.large; +package org.apache.jackrabbit.oak.plugins.index.old.mk.simple; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.NodeImpl; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.SimpleKernelImpl; +import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import org.apache.jackrabbit.mk.MultiMkTestBase; -import org.apache.jackrabbit.mk.simple.NodeImpl; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; /** * Test moving nodes. */ -@RunWith(Parameterized.class) -public class NodeVersionTest extends MultiMkTestBase { +public class NodeVersionTest { - private String head; - - public NodeVersionTest(String url) { - super(url); - } - - @Before - public void setUp() throws Exception { - super.setUp(); - } - - @After - public void tearDown() throws InterruptedException { - if (isSimpleKernel(mk)) { - head = mk.commit("/:root/head/config", "^ \"nodeVersion\": false", head, ""); - head = mk.commit("/:root/head/config", "^ \"nodeVersion\": null", head, ""); - } - super.tearDown(); - } + private final MicroKernel mk = new SimpleKernelImpl("mem:NodeVersionTest"); @Test public void nodeVersion() { - if (!isSimpleKernel(mk)) { - return; - } String head = mk.getHeadRevision(); head = mk.commit("/:root/head/config", "^ \"nodeVersion\": true", head, ""); head = mk.commit("/", "+ \"test1\": { \"id\": 1 }", head, ""); head = mk.commit("/", "+ \"test2\": { \"id\": 1 }", head, ""); - NodeImpl n = NodeImpl.parse(mk.getNodes("/", head)); + NodeImpl n = NodeImpl.parse(mk.getNodes("/", head, 1, 0, -1, null)); String vra = n.getNodeVersion(); String v1a = n.getNode("test1").getNodeVersion(); String v2a = n.getNode("test2").getNodeVersion(); @@ -71,7 +47,7 @@ public void nodeVersion() { // changes the node version head = mk.commit("/", "^ \"test2/id\": 2", head, ""); - n = NodeImpl.parse(mk.getNodes("/", head)); + n = NodeImpl.parse(mk.getNodes("/", head, 1, 0, -1, null)); String vrb = n.getNodeVersion(); String v1b = n.getNode("test1").getNodeVersion(); String v2b = n.getNode("test2").getNodeVersion(); diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/SimpleKernelImplFixture.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/SimpleKernelImplFixture.java new file mode 100644 index 00000000000..f073e8d46dd --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/SimpleKernelImplFixture.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.plugins.index.old.mk.simple; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.test.MicroKernelFixture; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.SimpleKernelImpl; + +public class SimpleKernelImplFixture implements MicroKernelFixture { + + @Override + public void setUpCluster(MicroKernel[] cluster) { + MicroKernel mk = + new SimpleKernelImpl("mem:SimpleKernelImplFixture"); + for (int i = 0; i < cluster.length; i++) { + cluster[i] = mk; + } + } + + @Override + public void syncMicroKernelCluster(MicroKernel... nodes) { + } + + @Override + public void tearDownCluster(MicroKernel[] cluster) { + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/SimpleKernelTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/SimpleKernelTest.java new file mode 100644 index 00000000000..197af3fb01c --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/simple/SimpleKernelTest.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.plugins.index.old.mk.simple; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.SimpleKernelImpl; +import org.junit.Test; + +/** + * {@link MicroKernel} test cases that currently only work against the + * {@link SimpleKernelImpl} implementation. + *

    + * TODO: Review these to see if they rely on implementation-specific + * functionality or if they should be turned into generic integration + * tests and the respective test failures in other MK implementations + * fixed. + */ +public class SimpleKernelTest { + + private final MicroKernel mk = new SimpleKernelImpl("mem:SimpleKernelTest"); + + @Test + public void reorderNode() { + String head = mk.getHeadRevision(); + String node = "reorderNode_" + System.currentTimeMillis(); + head = mk.commit("/", "+\"" + node + "\" : {\"a\":{}, \"b\":{}, \"c\":{}}", head, ""); + // System.out.println(mk.getNodes('/' + node, head).replaceAll("\"", "").replaceAll(":childNodeCount:.", "")); + + head = mk.commit("/", ">\"" + node + "/a\" : {\"before\":\"" + node + "/c\"}", head, ""); + // System.out.println(mk.getNodes('/' + node, head).replaceAll("\"", "").replaceAll(":childNodeCount:.", "")); + } + + @Test + public void doubleDelete() { + String head = mk.getHeadRevision(); + head = mk.commit("/", "+\"a\": {}", head, ""); + mk.commit("/", "-\"a\"", head, ""); + mk.commit("/", "-\"a\"", head, ""); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/wrapper/IndexWrapperTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/IndexWrapperTest.java similarity index 53% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/wrapper/IndexWrapperTest.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/IndexWrapperTest.java index 220f022d663..04f3c97700a 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/wrapper/IndexWrapperTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/IndexWrapperTest.java @@ -14,82 +14,70 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.wrapper; +package org.apache.jackrabbit.oak.plugins.index.old.mk.wrapper; +import static org.apache.jackrabbit.oak.plugins.index.old.Indexer.INDEX_CONFIG_PATH; import static org.junit.Assert.assertEquals; -import org.apache.jackrabbit.mk.MicroKernelFactory; -import org.apache.jackrabbit.mk.MultiMkTestBase; -import org.junit.Before; +import static org.junit.Assert.assertNull; +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.oak.plugins.index.old.mk.IndexWrapper; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.SimpleKernelImpl; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; /** * Test the index wrapper. */ -@RunWith(Parameterized.class) -public class IndexWrapperTest extends MultiMkTestBase { +public class IndexWrapperTest { - private String head; + // TODO: Remove SimpleKernelImpl-specific assumptions from the test + private final MicroKernel mk = + new IndexWrapper(new SimpleKernelImpl("mem:IndexWrapperTest")); - public IndexWrapperTest(String url) { - super(url); - } + private String head; - @Before - public void setUp() throws Exception { - super.setUp(); - mk.dispose(); - url = "index:" + url; - mk = MicroKernelFactory.getInstance(url); + @Test + public void getNodes() { + assertNull(mk.getNodes(INDEX_CONFIG_PATH + "/unknown", head, 1, 0, -1, null)); + assertNull(mk.getNodes("/unknown", head, 1, 0, -1, null)); } @Test public void prefix() { - if (!isSimpleKernel(mk)) { - return; - } - head = mk.commit("/index", "+ \"prefix:x\": {}", head, ""); + head = mk.commit(INDEX_CONFIG_PATH, "+ \"prefix@x\": {}", head, ""); head = mk.commit("/", "+ \"n1\": { \"value\":\"a:no\" }", head, ""); head = mk.commit("/", "+ \"n2\": { \"value\":\"x:yes\" }", head, ""); head = mk.commit("/", "+ \"n3\": { \"value\":\"x:a\" }", head, ""); head = mk.commit("/", "+ \"n4\": { \"value\":\"x:a\" }", head, ""); - String empty = mk.getNodes("/index/prefix:x?x:no", head); + String empty = mk.getNodes(INDEX_CONFIG_PATH + "/prefix@x?x:no", head, 1, 0, -1, null); assertEquals("[]", empty); - String yes = mk.getNodes("/index/prefix:x?x:yes", head); + String yes = mk.getNodes(INDEX_CONFIG_PATH + "/prefix@x?x:yes", head, 1, 0, -1, null); assertEquals("[\"/n2/value\"]", yes); - String a = mk.getNodes("/index/prefix:x?x:a", head); + String a = mk.getNodes(INDEX_CONFIG_PATH + "/prefix@x?x:a", head, 1, 0, -1, null); assertEquals("[\"/n3/value\",\"/n4/value\"]", a); } @Test public void propertyUnique() { - if (!isSimpleKernel(mk)) { - return; - } - head = mk.commit("/index", "+ \"property:id,unique\": {}", head, ""); + head = mk.commit(INDEX_CONFIG_PATH, "+ \"property@id,unique\": {}", head, ""); head = mk.commit("/", "+ \"n1\": { \"value\":\"empty\" }", head, ""); head = mk.commit("/", "+ \"n2\": { \"id\":\"1\" }", head, ""); - String empty = mk.getNodes("/index/property:id,unique?0", head); + String empty = mk.getNodes(INDEX_CONFIG_PATH + "/property@id,unique?0", head, 1, 0, -1, null); assertEquals("[]", empty); - String one = mk.getNodes("/index/property:id,unique?1", head); + String one = mk.getNodes(INDEX_CONFIG_PATH + "/property@id,unique?1", head, 1, 0, -1, null); assertEquals("[\"/n2\"]", one); } @Test public void propertyNonUnique() { - if (!isSimpleKernel(mk)) { - return; - } - head = mk.commit("/index", "+ \"property:ref\": {}", head, ""); + head = mk.commit(INDEX_CONFIG_PATH, "+ \"property@ref\": {}", head, ""); head = mk.commit("/", "+ \"n1\": { \"ref\":\"a\" }", head, ""); head = mk.commit("/", "+ \"n2\": { \"ref\":\"b\" }", head, ""); head = mk.commit("/", "+ \"n3\": { \"ref\":\"b\" }", head, ""); - String empty = mk.getNodes("/index/property:ref?no", head); + String empty = mk.getNodes(INDEX_CONFIG_PATH + "/property@ref?no", head, 1, 0, -1, null); assertEquals("[]", empty); - String one = mk.getNodes("/index/property:ref?a", head); + String one = mk.getNodes(INDEX_CONFIG_PATH + "/property@ref?a", head, 1, 0, -1, null); assertEquals("[\"/n1\"]", one); - String two = mk.getNodes("/index/property:ref?b", head); + String two = mk.getNodes(INDEX_CONFIG_PATH + "/property@ref?b", head, 1, 0, -1, null); assertEquals("[\"/n2\",\"/n3\"]", two); } diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/wrapper/SecurityWrapperTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/SecurityWrapperTest.java similarity index 78% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/wrapper/SecurityWrapperTest.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/SecurityWrapperTest.java index f4e378fc3ab..c25d734ae7b 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/wrapper/SecurityWrapperTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/SecurityWrapperTest.java @@ -14,71 +14,49 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.wrapper; +package org.apache.jackrabbit.oak.plugins.index.old.mk.wrapper; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import junit.framework.Assert; -import org.apache.jackrabbit.mk.MicroKernelFactory; -import org.apache.jackrabbit.mk.MultiMkTestBase; import org.apache.jackrabbit.mk.api.MicroKernel; import org.apache.jackrabbit.mk.api.MicroKernelException; +import org.apache.jackrabbit.mk.json.JsopReader; import org.apache.jackrabbit.mk.json.JsopTokenizer; -import org.junit.After; +import org.apache.jackrabbit.oak.plugins.index.old.mk.simple.SimpleKernelImpl; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; /** * Test the security wrapper. */ -@RunWith(Parameterized.class) -public class SecurityWrapperTest extends MultiMkTestBase { +public class SecurityWrapperTest { + + // TODO: Remove SimpleKernelImpl-specific assumptions from the test + private final MicroKernel mk = + new SimpleKernelImpl("mem:SecurityWrapperTest"); private String head; private MicroKernel mkAdmin; private MicroKernel mkGuest; - public SecurityWrapperTest(String url) { - super(url); - } - @Before public void setUp() throws Exception { - super.setUp(); - if (!isSimpleKernel(mk)) { - return; - } head = mk.getHeadRevision(); head = mk.commit("/", "+ \":user\": { \":rights\":\"admin\" }", head, ""); head = mk.commit("/", "+ \":user/guest\": {\"password\": \"guest\", \"rights\":\"read\" }", head, ""); head = mk.commit("/", "+ \":user/sa\": {\"password\": \"abc\", \"rights\":\"admin\" }", head, ""); - mkAdmin = MicroKernelFactory.getInstance("sec:sa@abc:" + url); - mkGuest = MicroKernelFactory.getInstance("sec:guest@guest:" + url); - } - - @After - public void tearDown() throws InterruptedException { - try { - if (mkAdmin != null) { - mkAdmin.dispose(); - } - if (mkGuest != null) { - mkGuest.dispose(); - } - super.tearDown(); - } catch (Throwable e) { - e.printStackTrace(); - } + mkAdmin = new SecurityWrapper(mk, "sa", "abc"); + mkGuest = new SecurityWrapper(mk, "guest", "guest"); } @Test public void wrongPassword() { try { - MicroKernelFactory.getInstance("sec:sa@xyz:" + url); + new SecurityWrapper(mk, "sa", "xyz"); fail(); } catch (Throwable e) { // expected (wrong password) @@ -87,9 +65,6 @@ public void wrongPassword() { @Test public void commit() { - if (!isSimpleKernel(mk)) { - return; - } head = mkAdmin.commit("/", "+ \"test\": { \"data\": \"Hello\" }", head, null); head = mkAdmin.commit("/", "- \"test\"", head, null); try { @@ -102,9 +77,6 @@ public void commit() { @Test public void getJournal() { - if (!isSimpleKernel(mk)) { - return; - } String fromRevision = mkAdmin.getHeadRevision(); String toRevision = mkAdmin.commit("/", "+ \"test\": { \"data\": \"Hello\" }", head, ""); toRevision = mkAdmin.commit("/", "^ \"test/data\": \"Hallo\"", toRevision, ""); @@ -129,21 +101,22 @@ public void getJournal() { @Test public void getNodes() { - if (!isSimpleKernel(mk)) { - return; - } head = mk.getHeadRevision(); assertTrue(mkAdmin.nodeExists("/:user", head)); assertFalse(mkGuest.nodeExists("/:user", head)); + assertNull(mkGuest.getNodes("/:user", head, 1, 0, -1, null)); head = mkAdmin.commit("/", "^ \":rights\": \"read\"", head, ""); head = mkAdmin.commit("/", "+ \"test\": { \"data\": \"Hello\" }", head, ""); assertTrue(mkAdmin.nodeExists("/", head)); + assertNull(mkGuest.getNodes("/unknown", head, 1, 0, -1, null)); + assertNull(mkGuest.getNodes("/unknown/node", head, 1, 0, -1, null)); assertTrue(mkGuest.nodeExists("/", head)); - assertEquals("{\":rights\":\"read\",\":childNodeCount\":2,\":user\":{\":rights\":\"admin\",\":childNodeCount\":2,\"guest\":{},\"sa\":{}},\"test\":{\"data\":\"Hello\",\":childNodeCount\":0}}", mkAdmin.getNodes("/", head)); - assertEquals("{\":childNodeCount\":1,\"test\":{\"data\":\"Hello\",\":childNodeCount\":0}}", mkGuest.getNodes("/", head)); + assertNull(mkGuest.getNodes("/unknown", head, 1, 0, -1, null)); + assertEquals("{\":rights\":\"read\",\":childNodeCount\":2,\":user\":{\":rights\":\"admin\",\":childNodeCount\":2,\"guest\":{},\"sa\":{}},\"test\":{\"data\":\"Hello\",\":childNodeCount\":0}}", mkAdmin.getNodes("/", head, 1, 0, -1, null)); + assertEquals("{\":childNodeCount\":1,\"test\":{\"data\":\"Hello\",\":childNodeCount\":0}}", mkGuest.getNodes("/", head, 1, 0, -1, null)); } - private String filterJournal(String journal) { + private static String filterJournal(String journal) { JsopTokenizer t = new JsopTokenizer(journal); StringBuilder buff = new StringBuilder(); t.read('['); @@ -156,7 +129,7 @@ private String filterJournal(String journal) { t.read(','); Assert.assertEquals("ts", t.readString()); t.read(':'); - t.read(JsopTokenizer.NUMBER); + t.read(JsopReader.NUMBER); t.read(','); Assert.assertEquals("msg", t.readString()); t.read(':'); diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/wrapper/VirtualRepositoryWrapperTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/VirtualRepositoryWrapperTest.java similarity index 84% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/wrapper/VirtualRepositoryWrapperTest.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/VirtualRepositoryWrapperTest.java index 662c807983d..1dccf459496 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/wrapper/VirtualRepositoryWrapperTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/old/mk/wrapper/VirtualRepositoryWrapperTest.java @@ -11,17 +11,18 @@ * KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -package org.apache.jackrabbit.mk.wrapper; +package org.apache.jackrabbit.oak.plugins.index.old.mk.wrapper; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.io.ByteArrayInputStream; +import java.util.Arrays; import java.util.Random; -import org.apache.jackrabbit.mk.MicroKernelFactory; -import org.apache.jackrabbit.mk.MultiMkTestBase; import org.apache.jackrabbit.mk.api.MicroKernel; -import org.apache.jackrabbit.mk.util.IOUtilsTest; +import org.apache.jackrabbit.oak.plugins.index.old.mk.MicroKernelFactory; +import org.apache.jackrabbit.oak.plugins.index.old.mk.MultiMkTestBase; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -43,6 +44,7 @@ public VirtualRepositoryWrapperTest(String url) { super(url); } + @Override @Before public void setUp() throws Exception { super.setUp(); @@ -59,20 +61,17 @@ public void setUp() throws Exception { mkVirtual = MicroKernelFactory.getInstance("virtual:" + url1); } + @Override @After public void tearDown() throws InterruptedException { try { mkRep1.commit("/", "- \":mount\"", mkRep1.getHeadRevision(), ""); mkRep2.commit("/", "- \":mount\"", mkRep2.getHeadRevision(), ""); if (mkVirtual != null) { - mkVirtual.dispose(); - } - if (mkRep1 != null) { - mkRep1.dispose(); - } - if (mkRep2 != null) { - mkRep2.dispose(); + MicroKernelFactory.disposeInstance(mkVirtual); } + MicroKernelFactory.disposeInstance(mkRep1); + MicroKernelFactory.disposeInstance(mkRep2); super.tearDown(); } catch (Throwable e) { e.printStackTrace(); @@ -89,35 +88,43 @@ public void commit() { head = mkVirtual.commit("/", "+ \"data\": {} ", head, ""); head = mkVirtual.commit("/", "+ \"data/a\": { \"data\": \"Hello\" }", head, ""); head = mkVirtual.commit("/", "+ \"data/b\": { \"data\": \"World\" }", head, ""); - String m1 = mkRep1.getNodes("/data", mkRep1.getHeadRevision()); + + // get nodes + String m1 = mkRep1.getNodes("/data", mkRep1.getHeadRevision(), 1, 0, -1, null); assertEquals("{\":childNodeCount\":1,\"a\":{\"data\":\"Hello\",\":childNodeCount\":0}}", m1); - String m2 = mkRep2.getNodes("/data", mkRep2.getHeadRevision()); + String m2 = mkRep2.getNodes("/data", mkRep2.getHeadRevision(), 1, 0, -1, null); assertEquals("{\":childNodeCount\":1,\"b\":{\"data\":\"World\",\":childNodeCount\":0}}", m2); - String m = mkVirtual.getNodes("/data/a", mkVirtual.getHeadRevision()); + String m = mkVirtual.getNodes("/data/a", mkVirtual.getHeadRevision(), 1, 0, -1, null); assertEquals("{\"data\":\"Hello\",\":childNodeCount\":0}", m); - m = mkVirtual.getNodes("/data/b", mkVirtual.getHeadRevision()); + m = mkVirtual.getNodes("/data/b", mkVirtual.getHeadRevision(), 1, 0, -1, null); assertEquals("{\"data\":\"World\",\":childNodeCount\":0}", m); + // get nodes on unknown nodes + m = mkVirtual.getNodes("/notMapped", mkVirtual.getHeadRevision(), 1, 0, -1, null); + assertNull(m); + m = mkVirtual.getNodes("/data/a/notExist", mkVirtual.getHeadRevision(), 1, 0, -1, null); + assertNull(m); + // set property head = mkVirtual.commit("/", "^ \"data/a/data\": \"Hallo\"", head, ""); head = mkVirtual.commit("/", "^ \"data/b/data\": \"Welt\"", head, ""); - m = mkVirtual.getNodes("/data/a", mkVirtual.getHeadRevision()); + m = mkVirtual.getNodes("/data/a", mkVirtual.getHeadRevision(), 1, 0, -1, null); assertEquals("{\"data\":\"Hallo\",\":childNodeCount\":0}", m); - m = mkVirtual.getNodes("/data/b", mkVirtual.getHeadRevision()); + m = mkVirtual.getNodes("/data/b", mkVirtual.getHeadRevision(), 1, 0, -1, null); assertEquals("{\"data\":\"Welt\",\":childNodeCount\":0}", m); // add property head = mkVirtual.commit("/", "+ \"data/a/lang\": \"de\"", head, ""); - m = mkVirtual.getNodes("/data/a", mkVirtual.getHeadRevision()); + m = mkVirtual.getNodes("/data/a", mkVirtual.getHeadRevision(), 1, 0, -1, null); assertEquals("{\"data\":\"Hallo\",\"lang\":\"de\",\":childNodeCount\":0}", m); head = mkVirtual.commit("/", "^ \"data/a/lang\": null", head, ""); - m = mkVirtual.getNodes("/data/a", mkVirtual.getHeadRevision()); + m = mkVirtual.getNodes("/data/a", mkVirtual.getHeadRevision(), 1, 0, -1, null); assertEquals("{\"data\":\"Hallo\",\":childNodeCount\":0}", m); // move head = mkVirtual.commit("/", "+ \"data/a/sub\": {}", head, ""); head = mkVirtual.commit("/", "> \"data/a/sub\": \"data/a/sub2\"", head, ""); - m = mkVirtual.getNodes("/data/a", mkVirtual.getHeadRevision()); + m = mkVirtual.getNodes("/data/a", mkVirtual.getHeadRevision(), 1, 0, -1, null); assertEquals("{\"data\":\"Hallo\",\":childNodeCount\":1,\"sub2\":{\":childNodeCount\":0}}", m); head = mkVirtual.commit("/", "- \"data/a/sub2\"", head, ""); @@ -136,7 +143,7 @@ public void binary() { String id = mkVirtual.write(new ByteArrayInputStream(data)); byte[] test = new byte[len]; mkVirtual.read(id, 0, test, 0, len); - IOUtilsTest.assertEquals(data, test); + assertTrue(Arrays.equals(data, test)); assertEquals(len, mkVirtual.getLength(id)); } diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexQueryTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexQueryTest.java new file mode 100644 index 00000000000..039fb3c8bed --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexQueryTest.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.plugins.index.property; + +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.plugins.index.IndexHookManager; +import org.apache.jackrabbit.oak.plugins.nodetype.InitialContent; +import org.apache.jackrabbit.oak.query.AbstractQueryTest; + +/** + * Tests the query engine using the default index implementation: the + * {@link PropertyIndexProvider} + */ +public class PropertyIndexQueryTest extends AbstractQueryTest { + + @Override + protected ContentRepository createRepository() { + return new Oak() + .with(new InitialContent()) + .with(new PropertyIndexProvider()) + .with(new IndexHookManager(new PropertyIndexHookProvider())) + .createContentRepository(); + } + +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexTest.java new file mode 100644 index 00000000000..1184050eda5 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexTest.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.property; + +import java.util.Arrays; + +import com.google.common.collect.ImmutableSet; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.index.IndexHook; +import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeState; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class PropertyIndexTest { + + private static final int MANY = 100; + + @Test + public void testPropertyLookup() throws Exception { + NodeState root = MemoryNodeState.EMPTY_NODE; + + // Add index definition + NodeBuilder builder = root.builder(); + builder.child("oak:index").child("foo") + .setProperty("jcr:primaryType", "oak:queryIndexDefinition", Type.NAME) + .setProperty("propertyNames", "foo"); + NodeState before = builder.getNodeState(); + + // Add some content and process it through the property index hook + builder = before.builder(); + builder.child("a").setProperty("foo", "abc"); + builder.child("b").setProperty("foo", Arrays.asList("abc", "def"), Type.STRINGS); + // plus lots of dummy content to highlight the benefit of indexing + for (int i = 0; i < MANY; i++) { + builder.child("n" + i).setProperty("foo", "xyz"); + } + NodeState after = builder.getNodeState(); + + // First check lookups without an index + PropertyIndexLookup lookup = new PropertyIndexLookup(after); + long withoutIndex = System.nanoTime(); + assertEquals(ImmutableSet.of("a", "b"), lookup.find("foo", "abc")); + assertEquals(ImmutableSet.of("b"), lookup.find("foo", "def")); + assertEquals(ImmutableSet.of(), lookup.find("foo", "ghi")); + assertEquals(MANY, lookup.find("foo", "xyz").size()); + withoutIndex = System.nanoTime() - withoutIndex; + + // ... then see how adding an index affects the code + IndexHook p = new PropertyIndexHook(builder); + after.compareAgainstBaseState(before, p.preProcess()); + p.postProcess(); + p.close(); + + lookup = new PropertyIndexLookup(builder.getNodeState()); + long withIndex = System.nanoTime(); + assertEquals(ImmutableSet.of("a", "b"), lookup.find("foo", "abc")); + assertEquals(ImmutableSet.of("b"), lookup.find("foo", "def")); + assertEquals(ImmutableSet.of(), lookup.find("foo", "ghi")); + assertEquals(MANY, lookup.find("foo", "xyz").size()); + withIndex = System.nanoTime() - withIndex; + + // System.out.println("Index performance ratio: " + withoutIndex/withIndex); + // assertTrue(withoutIndex > withIndex); + } + + @Test + public void testCustomConfigPropertyLookup() throws Exception { + NodeState root = MemoryNodeState.EMPTY_NODE; + + // Add index definition + NodeBuilder builder = root.builder(); + builder.child("oak:index").child("fooIndex") + .setProperty("jcr:primaryType", "oak:queryIndexDefinition", Type.NAME) + .setProperty("propertyNames", Arrays.asList("foo", "extrafoo"), Type.STRINGS); + NodeState before = builder.getNodeState(); + + // Add some content and process it through the property index hook + builder = before.builder(); + builder.child("a").setProperty("foo", "abc").setProperty("extrafoo", "pqr"); + builder.child("b").setProperty("foo", Arrays.asList("abc", "def"), Type.STRINGS); + // plus lots of dummy content to highlight the benefit of indexing + for (int i = 0; i < MANY; i++) { + builder.child("n" + i).setProperty("foo", "xyz"); + } + NodeState after = builder.getNodeState(); + + // First check lookups without an index + PropertyIndexLookup lookup = new PropertyIndexLookup(after); + long withoutIndex = System.nanoTime(); + assertEquals(ImmutableSet.of("a", "b"), lookup.find("foo", "abc")); + assertEquals(ImmutableSet.of("b"), lookup.find("foo", "def")); + assertEquals(ImmutableSet.of(), lookup.find("foo", "ghi")); + assertEquals(MANY, lookup.find("foo", "xyz").size()); + assertEquals(ImmutableSet.of("a"), lookup.find("extrafoo", "pqr")); + assertEquals(ImmutableSet.of(), lookup.find("pqr", "foo")); + withoutIndex = System.nanoTime() - withoutIndex; + + // ... then see how adding an index affects the code + IndexHook p = new PropertyIndexHook(builder); + after.compareAgainstBaseState(before, p.preProcess()); + p.postProcess(); + p.close(); + + lookup = new PropertyIndexLookup(builder.getNodeState()); + long withIndex = System.nanoTime(); + assertEquals(ImmutableSet.of("a", "b"), lookup.find("foo", "abc")); + assertEquals(ImmutableSet.of("b"), lookup.find("foo", "def")); + assertEquals(ImmutableSet.of(), lookup.find("foo", "ghi")); + assertEquals(MANY, lookup.find("foo", "xyz").size()); + assertEquals(ImmutableSet.of("a"), lookup.find("extrafoo", "pqr")); + assertEquals(ImmutableSet.of(), lookup.find("pqr", "foo")); + withIndex = System.nanoTime() - withIndex; + + // System.out.println("Index performance ratio: " + withoutIndex/withIndex); + // assertTrue(withoutIndex > withIndex); + } + + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/memory/MemoryNodeBuilderTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/memory/MemoryNodeBuilderTest.java new file mode 100644 index 00000000000..e31e497005d --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/memory/MemoryNodeBuilderTest.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import com.google.common.collect.ImmutableMap; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.fail; +import static org.apache.jackrabbit.oak.api.Type.STRING; + +public class MemoryNodeBuilderTest { + + private static final NodeState BASE = new MemoryNodeState( + ImmutableMap.of( + "a", LongPropertyState.createLongProperty("a", 1L), + "b", LongPropertyState.createLongProperty("b", 2L), + "c", LongPropertyState.createLongProperty("c", 3L)), + ImmutableMap.of( + "x", MemoryNodeState.EMPTY_NODE, + "y", MemoryNodeState.EMPTY_NODE, + "z", MemoryNodeState.EMPTY_NODE)); + + @Test + public void testConnectOnAddProperty() { + NodeBuilder root = new MemoryNodeBuilder(BASE); + NodeBuilder childA = root.child("x"); + NodeBuilder childB = root.child("x"); + + assertNull(childA.getProperty("test")); + childB.setProperty("test", "foo"); + assertNotNull(childA.getProperty("test")); + } + + @Test + public void testConnectOnUpdateProperty() { + NodeBuilder root = new MemoryNodeBuilder(BASE); + NodeBuilder childA = root.child("x"); + NodeBuilder childB = root.child("x"); + + childB.setProperty("test", "foo"); + + childA.setProperty("test", "bar"); + assertEquals("bar", childA.getProperty("test").getValue(STRING)); + assertEquals("bar", childB.getProperty("test").getValue(STRING)); + } + + @Test + public void testConnectOnRemoveProperty() { + NodeBuilder root = new MemoryNodeBuilder(BASE); + NodeBuilder childA = root.child("x"); + NodeBuilder childB = root.child("x"); + + childB.setProperty("test", "foo"); + + childA.removeProperty("test"); + assertNull(childA.getProperty("test")); + assertNull(childB.getProperty("test")); + + childA.setProperty("test", "bar"); + assertEquals("bar", childA.getProperty("test").getValue(STRING)); + assertEquals("bar", childB.getProperty("test").getValue(STRING)); + } + + @Test + public void testConnectOnAddNode() { + NodeBuilder root = new MemoryNodeBuilder(BASE); + NodeBuilder childA = root.child("x"); + NodeBuilder childB = root.child("x"); + + assertFalse(childA.hasChildNode("test")); + assertFalse(childB.hasChildNode("test")); + + childB.child("test"); + assertTrue(childA.hasChildNode("test")); + assertTrue(childB.hasChildNode("test")); + } + + @Test + public void testConnectOnRemoveNode() { + NodeBuilder root = new MemoryNodeBuilder(BASE); + NodeBuilder child = root.child("x"); + + root.removeNode("x"); + try { + child.getChildNodeCount(); + fail(); + } catch (IllegalStateException e) { + // expected + } + + root.child("x"); + assertEquals(0, child.getChildNodeCount()); // reconnect! + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/memory/MemoryPropertyBuilderTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/memory/MemoryPropertyBuilderTest.java new file mode 100644 index 00000000000..e2593870623 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/memory/MemoryPropertyBuilderTest.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.memory; + +import java.util.Arrays; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.spi.state.PropertyBuilder; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class MemoryPropertyBuilderTest { + + @Test + public void testStringProperty() { + PropertyBuilder builder = MemoryPropertyBuilder.create(Type.STRING); + builder.setName("foo").setValue("bar"); + assertEquals(StringPropertyState.stringProperty("foo", "bar"), + builder.getPropertyState()); + + assertEquals(MultiStringPropertyState.stringProperty("foo", Arrays.asList("bar")), + builder.getPropertyState(true)); + } + + @Test + public void testLongProperty() { + PropertyBuilder builder = MemoryPropertyBuilder.create(Type.LONG); + builder.setName("foo").setValue(42L); + assertEquals(LongPropertyState.createLongProperty("foo", 42L), + builder.getPropertyState()); + + assertEquals(MultiLongPropertyState.createLongProperty("foo", Arrays.asList(42L)), + builder.getPropertyState(true)); + } + + @Test + public void testStringsProperty() { + PropertyBuilder builder = MemoryPropertyBuilder.create(Type.STRING); + builder.setName("foo") + .addValue("one") + .addValue("two"); + assertEquals(MultiStringPropertyState.stringProperty("foo", Arrays.asList("one", "two")), + builder.getPropertyState()); + } + + @Test + public void testAssignFromLong() { + PropertyState source = LongPropertyState.createLongProperty("foo", 42L); + PropertyBuilder builder = MemoryPropertyBuilder.create(Type.STRING); + builder.assignFrom(source); + assertEquals(StringPropertyState.stringProperty("foo", "42"), + builder.getPropertyState()); + } + + @Test + public void testAssignFromString() { + PropertyState source = StringPropertyState.stringProperty("foo", "42"); + PropertyBuilder builder = MemoryPropertyBuilder.create(Type.LONG); + builder.assignFrom(source); + assertEquals(LongPropertyState.createLongProperty("foo", 42L), + builder.getPropertyState()); + } + + @Test(expected = NumberFormatException.class) + public void testAssignFromStringNumberFormatException() { + PropertyState source = StringPropertyState.stringProperty("foo", "bar"); + PropertyBuilder builder = MemoryPropertyBuilder.create(Type.LONG); + builder.assignFrom(source); + } + + @Test + public void testAssignFromLongs() { + PropertyState source = MultiLongPropertyState.createLongProperty("foo", Arrays.asList(1L, 2L, 3L)); + PropertyBuilder builder = MemoryPropertyBuilder.create(Type.STRING); + builder.assignFrom(source); + assertEquals(MultiStringPropertyState.stringProperty("foo", Arrays.asList("1", "2", "3")), + builder.getPropertyState()); + } + + @Test + public void testAssignFromStrings() { + PropertyState source = MultiStringPropertyState.stringProperty("foo", Arrays.asList("1", "2", "3")); + PropertyBuilder builder = MemoryPropertyBuilder.create(Type.LONG); + builder.assignFrom(source); + assertEquals(MultiLongPropertyState.createLongProperty("foo", Arrays.asList(1L, 2L, 3L)), + builder.getPropertyState()); + } + + @Test + public void testAssignInvariant() { + PropertyState source = MultiStringPropertyState.stringProperty("source", Arrays.asList("1", "2", "3")); + PropertyBuilder builder = MemoryPropertyBuilder.create(Type.STRING); + builder.assignFrom(source); + assertEquals(source, builder.getPropertyState(true)); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/name/NameValidatorTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/name/NameValidatorTest.java new file mode 100644 index 00000000000..991dce9dd49 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/name/NameValidatorTest.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.name; + +import java.util.Collections; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeState; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.junit.Test; + +public class NameValidatorTest { + + private final Validator validator = + new NameValidator(Collections.singleton("valid")); + + @Test(expected = CommitFailedException.class) + public void testCurrentPath() throws CommitFailedException { + validator.childNodeAdded(".", MemoryNodeState.EMPTY_NODE); + } + + @Test(expected = CommitFailedException.class) + public void testParentPath() throws CommitFailedException { + validator.childNodeAdded("..", MemoryNodeState.EMPTY_NODE); + } + + @Test // valid as of OAK-182 + public void testEmptyPrefix() throws CommitFailedException { + validator.childNodeAdded(":name", MemoryNodeState.EMPTY_NODE); + } + + @Test(expected = CommitFailedException.class) + public void testInvalidPrefix() throws CommitFailedException { + validator.childNodeAdded("invalid:name", MemoryNodeState.EMPTY_NODE); + } + + @Test + public void testValidPrefix() throws CommitFailedException { + validator.childNodeAdded("valid:name", MemoryNodeState.EMPTY_NODE); + } + + @Test(expected = CommitFailedException.class) + public void testSlashName() throws CommitFailedException { + validator.childNodeAdded("invalid/name", MemoryNodeState.EMPTY_NODE); + } + + @Test(expected = CommitFailedException.class) + public void testIndexInName() throws CommitFailedException { + validator.childNodeAdded("name[1]", MemoryNodeState.EMPTY_NODE); + } + + @Test + public void testValidName() throws CommitFailedException { + validator.childNodeAdded("name", MemoryNodeState.EMPTY_NODE); + } + + @Test + public void testDeleted() throws CommitFailedException { + validator.childNodeDeleted(".", MemoryNodeState.EMPTY_NODE); + validator.childNodeDeleted("..", MemoryNodeState.EMPTY_NODE); + validator.childNodeDeleted("valid:name", MemoryNodeState.EMPTY_NODE); + validator.childNodeDeleted("invalid:name", MemoryNodeState.EMPTY_NODE); + validator.childNodeDeleted("invalid/name", MemoryNodeState.EMPTY_NODE); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/name/ReadWriteNamespaceRegistryTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/name/ReadWriteNamespaceRegistryTest.java new file mode 100644 index 00000000000..f3a5ec6389c --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/name/ReadWriteNamespaceRegistryTest.java @@ -0,0 +1,66 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one or more +* contributor license agreements. See the NOTICE file distributed with +* this work for additional information regarding copyright ownership. +* The ASF licenses this file to You under the Apache License, Version 2.0 +* (the "License"); you may not use this file except in compliance with +* the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package org.apache.jackrabbit.oak.plugins.name; + +import javax.jcr.NamespaceRegistry; + +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class ReadWriteNamespaceRegistryTest{ + + @Test + public void testMappings() throws Exception { + final ContentSession session = new Oak().createContentSession(); + NamespaceRegistry r = new ReadWriteNamespaceRegistry() { + @Override + protected Tree getReadTree() { + return session.getLatestRoot().getTree("/"); + } + @Override + protected Root getWriteRoot() { + return session.getLatestRoot(); + } + }; + + assertEquals("", r.getURI("")); + assertEquals("http://www.jcp.org/jcr/1.0", r.getURI("jcr")); + assertEquals("http://www.jcp.org/jcr/nt/1.0", r.getURI("nt")); + assertEquals("http://www.jcp.org/jcr/mix/1.0", r.getURI("mix")); + assertEquals("http://www.w3.org/XML/1998/namespace", r.getURI("xml")); + + assertEquals("", r.getPrefix("")); + assertEquals("jcr", r.getPrefix("http://www.jcp.org/jcr/1.0")); + assertEquals("nt", r.getPrefix("http://www.jcp.org/jcr/nt/1.0")); + assertEquals("mix", r.getPrefix("http://www.jcp.org/jcr/mix/1.0")); + assertEquals("xml", r.getPrefix("http://www.w3.org/XML/1998/namespace")); + + r.registerNamespace("p", "n"); + assertEquals(r.getURI("p"), "n"); + assertEquals(r.getPrefix("n"), "p"); + + r.registerNamespace("p2", "n2"); + assertEquals(r.getURI("p2"), "n2"); + assertEquals(r.getPrefix("n2"), "p2"); + + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/AbstractQueryTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/AbstractQueryTest.java new file mode 100644 index 00000000000..fc53e00b102 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/AbstractQueryTest.java @@ -0,0 +1,301 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.query; + +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.LineNumberReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.oak.api.ContentRepository; +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.ResultRow; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.SessionQueryEngine; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NODE_TYPE; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.REINDEX_PROPERTY_NAME; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.TYPE_PROPERTY_NAME; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * AbstractQueryTest... + */ +public abstract class AbstractQueryTest { + + protected static final String TEST_INDEX_NAME = "test-index"; + + protected SessionQueryEngine qe; + protected ContentSession session; + protected Root root; + + @Before + public void before() throws Exception { + session = createRepository().login(null, null); + root = session.getLatestRoot(); + qe = root.getQueryEngine(); + createTestIndexNode(); + } + + protected abstract ContentRepository createRepository(); + + /** + * Override this method to add your default index definition + * + * {@link #createTestIndexNode(Tree, String)} for a helper method + */ + protected void createTestIndexNode() throws Exception { + Tree index = root.getTree("/"); + createTestIndexNode(index, "unknown"); + root.commit(); + } + + protected static Tree createTestIndexNode(Tree index, String type) + throws Exception { + Tree indexDef = index.addChild(INDEX_DEFINITIONS_NAME).addChild( + TEST_INDEX_NAME); + indexDef.setProperty(JcrConstants.JCR_PRIMARYTYPE, + INDEX_DEFINITIONS_NODE_TYPE, Type.NAME); + indexDef.setProperty(TYPE_PROPERTY_NAME, type); + indexDef.setProperty(REINDEX_PROPERTY_NAME, true); + return indexDef; + } + + protected Result executeQuery(String statement, String language, + Map sv) throws ParseException { + return qe.executeQuery(statement, language, Long.MAX_VALUE, 0, sv, null); + } + + @Test + public void sql1() throws Exception { + test("sql1.txt"); + } + + @Test + public void sql2() throws Exception { + test("sql2.txt"); + } + + @Test + public void xpath() throws Exception { + test("xpath.txt"); + } + + @Test + @Ignore("OAK-336") + public void sql2Measure() throws Exception { + test("sql2_measure.txt"); + } + + @Test + public void bindVariableTest() throws Exception { + JsopUtil.apply( + root, + "/ + \"test\": { \"hello\": {\"id\": \"1\"}, \"world\": {\"id\": \"2\"}}"); + root.commit(); + + Map sv = new HashMap(); + sv.put("id", PropertyValues.newString("1")); + Iterator result; + result = executeQuery("select * from [nt:base] where id = $id", + QueryEngineImpl.SQL2, sv).getRows().iterator(); + assertTrue(result.hasNext()); + assertEquals("/test/hello", result.next().getPath()); + + sv.put("id", PropertyValues.newString("2")); + result = executeQuery("select * from [nt:base] where id = $id", + QueryEngineImpl.SQL2, sv).getRows().iterator(); + assertTrue(result.hasNext()); + assertEquals("/test/world", result.next().getPath()); + } + + protected void test(String file) throws Exception { + InputStream in = AbstractQueryTest.class.getResourceAsStream(file); + LineNumberReader r = new LineNumberReader(new InputStreamReader(in)); + PrintWriter w = new PrintWriter(new OutputStreamWriter( + new FileOutputStream("target/" + getClass().getName() + "_" + + file))); + HashSet knownQueries = new HashSet(); + boolean errors = false; + try { + while (true) { + String line = r.readLine(); + if (line == null) { + break; + } + line = line.trim(); + if (line.startsWith("#") || line.length() == 0) { + w.println(line); + } else if (line.startsWith("xpath2sql")) { + line = line.substring("xpath2sql".length()).trim(); + w.println("xpath2sql " + line); + XPathToSQL2Converter c = new XPathToSQL2Converter(); + String got; + try { + got = c.convert(line); + executeQuery(got, QueryEngineImpl.SQL2, null); + } catch (ParseException e) { + got = "invalid: " + e.getMessage().replace('\n', ' '); + } catch (Exception e) { + // e.printStackTrace(); + got = "error: " + e.toString().replace('\n', ' '); + } + if (!knownQueries.add(line)) { + got = "duplicate xpath2sql query"; + } + line = r.readLine().trim(); + w.println(got); + if (!line.equals(got)) { + errors = true; + } + } else if (line.startsWith("select") + || line.startsWith("explain") + || line.startsWith("measure") + || line.startsWith("sql1") || line.startsWith("xpath")) { + w.println(line); + String language = QueryEngineImpl.SQL2; + if (line.startsWith("sql1 ")) { + language = QueryEngineImpl.SQL; + line = line.substring("sql1 ".length()); + } else if (line.startsWith("xpath ")) { + language = QueryEngineImpl.XPATH; + line = line.substring("xpath ".length()); + } + boolean readEnd = true; + for (String resultLine : executeQuery(line, language)) { + w.println(resultLine); + if (readEnd) { + line = r.readLine(); + if (line == null) { + errors = true; + readEnd = false; + } else { + line = line.trim(); + if (line.length() == 0) { + errors = true; + readEnd = false; + } else { + if (!line.equals(resultLine)) { + errors = true; + } + } + } + } + } + w.println(""); + if (readEnd) { + while (true) { + line = r.readLine(); + if (line == null) { + break; + } + line = line.trim(); + if (line.length() == 0) { + break; + } + errors = true; + } + } + } else if (line.startsWith("commit")) { + w.println(line); + line = line.substring("commit".length()).trim(); + JsopUtil.apply(root, line); + root.commit(); + } + w.flush(); + } + } finally { + w.close(); + r.close(); + } + if (errors) { + throw new Exception("Results in target/" + file + + " don't match expected " + + "results in src/test/resources/" + file + + "; compare the files for details"); + } + } + + protected List executeQuery(String query, String language) { + long time = System.currentTimeMillis(); + List lines = new ArrayList(); + try { + Result result = executeQuery(query, language, null); + for (ResultRow row : result.getRows()) { + lines.add(readRow(row)); + } + if (!query.contains("order by")) { + Collections.sort(lines); + } + } catch (ParseException e) { + lines.add(e.toString()); + } catch (IllegalArgumentException e) { + lines.add(e.toString()); + } + time = System.currentTimeMillis() - time; + if (time > 3000 && !isDebugModeEnabled()) { + fail("Query took too long: " + query + " took " + time + " ms"); + } + return lines; + } + + protected static String readRow(ResultRow row) { + StringBuilder buff = new StringBuilder(); + PropertyValue[] values = row.getValues(); + for (int i = 0; i < values.length; i++) { + if (i > 0) { + buff.append(", "); + } + PropertyValue v = values[i]; + buff.append(v == null ? "null" : v.getValue(Type.STRING)); + } + return buff.toString(); + } + + /** + * Check whether the test is running in debug mode. + * + * @return true if debug most is (most likely) enabled + */ + protected static boolean isDebugModeEnabled() { + return java.lang.management.ManagementFactory.getRuntimeMXBean() + .getInputArguments().toString().indexOf("-agentlib:jdwp") > 0; + } + +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/JsopUtil.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/JsopUtil.java new file mode 100644 index 00000000000..bbe70a956d2 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/JsopUtil.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.query; + +import org.apache.jackrabbit.mk.json.JsopTokenizer; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.plugins.memory.PropertyStates; + +/** + * Utility class for working with jsop string diffs + * + */ +public class JsopUtil { + + private JsopUtil() { + + } + + /** + * Applies the commit string to a given Root instance + * + * + * The commit string represents a sequence of operations, jsonp style: + * + *

    + * / + "test": { "a": { "id": "ref:123" }, "b": { "id" : "str:123" }} + *

    + * or + *

    + * "/ - "test" + *

    + * + * @param root + * @param commit the commit string + * @throws UnsupportedOperationException if the operation is not supported + */ + public static void apply(Root root, String commit) + throws UnsupportedOperationException { + int index = commit.indexOf(' '); + String path = commit.substring(0, index).trim(); + Tree c = root.getTree(path); + if (c == null) { + // TODO create intermediary? + throw new UnsupportedOperationException("Non existing path " + path); + } + commit = commit.substring(index); + JsopTokenizer tokenizer = new JsopTokenizer(commit); + if (tokenizer.matches('-')) { + removeTree(c, tokenizer); + } else if (tokenizer.matches('+')) { + addTree(c, tokenizer); + } else { + throw new UnsupportedOperationException( + "Unsupported " + (char) tokenizer.read() + + ". This should be either '+' or '-'."); + } + } + + private static void removeTree(Tree t, JsopTokenizer tokenizer) { + String path = tokenizer.readString(); + for (String p : PathUtils.elements(path)) { + if (!t.hasChild(p)) { + return; + } + t = t.getChild(p); + } + t.remove(); + } + + private static void addTree(Tree t, JsopTokenizer tokenizer) { + do { + String key = tokenizer.readString(); + tokenizer.read(':'); + if (tokenizer.matches('{')) { + Tree c = t.addChild(key); + if (!tokenizer.matches('}')) { + addTree(c, tokenizer); + tokenizer.read('}'); + } + } else if (tokenizer.matches('[')) { + t.setProperty(PropertyStates.readArrayProperty(key, tokenizer, null)); + } else { + t.setProperty(PropertyStates.readProperty(key, tokenizer, null)); + } + } while (tokenizer.matches(',')); + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/ast/FullTextTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/ast/FullTextTest.java new file mode 100644 index 00000000000..04e79821ede --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/ast/FullTextTest.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.query.ast; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.text.ParseException; + +import org.junit.Test; + +/** + * Test the fulltext parsing and evaluation. + */ +public class FullTextTest { + + @Test + public void and() throws ParseException { + assertFalse(test("hello world", "hello")); + assertFalse(test("hello world", "world")); + assertTrue(test("hello world", "world hello")); + assertTrue(test("hello world ", "hello world")); + } + + @Test + public void or() throws ParseException { + assertTrue(test("hello OR world", "hello")); + assertTrue(test("hello OR world", "world")); + assertFalse(test("hello OR world", "hi")); + } + + @Test + public void not() throws ParseException { + assertTrue(test("hello -world", "hello")); + assertFalse(test("hello -world", "hello world")); + } + + @Test + public void quoted() throws ParseException { + assertTrue(test("\"hello world\"", "hello world")); + assertFalse(test("\"hello world\"", "world hello")); + assertTrue(test("\"hello-world\"", "hello-world")); + assertTrue(test("\"hello\\-world\"", "hello-world")); + assertTrue(test("\"hello \\\"world\\\"\"", "hello \"world\"")); + assertTrue(test("\"hello world\" -hallo", "hello world")); + assertFalse(test("\"hello world\" -hallo", "hallo hello world")); + } + + @Test + public void escaped() throws ParseException { + assertFalse(test("\\\"hello\\\"", "hello")); + assertTrue(test("\"hello\"", "\"hello\"")); + assertTrue(test("\\\"hello\\\"", "\"hello\"")); + assertFalse(test("\\-1 2 3", "1 2 3")); + assertTrue(test("\\-1 2 3", "-1 2 3")); + } + + @Test + public void invalid() throws ParseException { + testInvalid("", "(*); expected: term"); + testInvalid("x OR ", "x OR(*); expected: term"); + testInvalid("\"", "(*)\"; expected: double quote"); + testInvalid("-", "(*)-; expected: term"); + testInvalid("- x", "- (*)x; expected: term"); + testInvalid("\\", "(*)\\; expected: escaped char"); + testInvalid("\"\\", "\"(*)\\; expected: escaped char"); + testInvalid("\"x\"y", "\"x\"(*)y; expected: space"); + } + + private static void testInvalid(String pattern, String expectedMessage) { + try { + test(pattern, ""); + fail("Expected exception " + expectedMessage); + } catch (ParseException e) { + String msg = e.getMessage(); + assertTrue(msg.startsWith("FullText expression: ")); + msg = msg.substring("FullText expression: ".length()); + assertEquals(expectedMessage, msg); + } + } + + private static boolean test(String pattern, String value) throws ParseException { + FullTextSearchImpl.FullTextExpression e = FullTextSearchImpl.FullTextParser.parse(pattern); + return e.evaluate(value); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/ast/LikePatternTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/ast/LikePatternTest.java new file mode 100644 index 00000000000..d7d33a0dd9a --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/ast/LikePatternTest.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.query.ast; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import org.junit.Test; + +/** + * Tests "like" pattern matching. + */ +public class LikePatternTest { + + @Test + public void pattern() { + pattern("%_", "X", "", null, null); + pattern("A%", "A", "X", "A", "B"); + pattern("A%%", "A", "X", "A", "B"); + pattern("%\\_%", "A_A", "AAA", null, null); + } + + private static void pattern(String pattern, String match, String noMatch, String lower, String upper) { + ComparisonImpl.LikePattern p = new ComparisonImpl.LikePattern(pattern); + if (match != null) { + assertTrue(p.matches(match)); + } + if (noMatch != null) { + assertFalse(p.matches(noMatch)); + } + assertEquals(lower, p.getLowerBound()); + assertEquals(upper, p.getUpperBound()); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/index/FilterTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/index/FilterTest.java new file mode 100644 index 00000000000..3b354435f57 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/index/FilterTest.java @@ -0,0 +1,272 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.index; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Random; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.query.ast.Operator; +import org.apache.jackrabbit.oak.spi.query.Filter; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; +import org.junit.Test; + +/** + * Tests the Filter class. + */ +public class FilterTest { + + @Test + public void propertyRestriction() { + + PropertyValue one = PropertyValues.newString("1"); + PropertyValue two = PropertyValues.newString("2"); + + FilterImpl f = new FilterImpl(null); + assertTrue(null == f.getPropertyRestriction("x")); + f.restrictProperty("x", Operator.LESS_OR_EQUAL, two); + assertEquals("..2]", f.getPropertyRestriction("x").toString()); + f.restrictProperty("x", Operator.GREATER_OR_EQUAL, one); + assertEquals("[1..2]", f.getPropertyRestriction("x").toString()); + f.restrictProperty("x", Operator.GREATER_THAN, one); + assertEquals("(1..2]", f.getPropertyRestriction("x").toString()); + f.restrictProperty("x", Operator.LESS_THAN, two); + assertEquals("(1..2)", f.getPropertyRestriction("x").toString()); + f.restrictProperty("x", Operator.EQUAL, two); + assertTrue(f.isAlwaysFalse()); + + f = new FilterImpl(null); + f.restrictProperty("x", Operator.EQUAL, one); + assertEquals("[1..1]", f.getPropertyRestriction("x").toString()); + f.restrictProperty("x", Operator.EQUAL, one); + assertEquals("[1..1]", f.getPropertyRestriction("x").toString()); + f.restrictProperty("x", Operator.GREATER_OR_EQUAL, one); + assertEquals("[1..1]", f.getPropertyRestriction("x").toString()); + f.restrictProperty("x", Operator.LESS_OR_EQUAL, one); + assertEquals("[1..1]", f.getPropertyRestriction("x").toString()); + f.restrictProperty("x", Operator.GREATER_THAN, one); + assertTrue(f.isAlwaysFalse()); + + f = new FilterImpl(null); + f.restrictProperty("x", Operator.EQUAL, one); + assertEquals("[1..1]", f.getPropertyRestriction("x").toString()); + f.restrictProperty("x", Operator.LESS_THAN, one); + assertTrue(f.isAlwaysFalse()); + + f = new FilterImpl(null); + f.restrictProperty("x", Operator.NOT_EQUAL, null); + assertEquals("..", f.getPropertyRestriction("x").toString()); + f.restrictProperty("x", Operator.LESS_THAN, one); + assertEquals("..1)", f.getPropertyRestriction("x").toString()); + f.restrictProperty("x", Operator.EQUAL, two); + assertTrue(f.isAlwaysFalse()); + + } + + @Test + public void pathRestrictionsRandomized() throws Exception { + ArrayList paths = new ArrayList(); + // create paths /a, /b, /c, /a/a, /a/b, ... /c/c/c + paths.add("/"); + for (int i = 'a'; i <= 'c'; i++) { + String p1 = "/" + (char) i; + paths.add(p1); + for (int j = 'a'; j <= 'c'; j++) { + String p2 = "/" + (char) j; + paths.add(p1 + p2); + for (int k = 'a'; k <= 'c'; k++) { + String p3 = "/" + (char) k; + paths.add(p1 + p2 + p3); + } + } + } + Random r = new Random(1); + for (int i = 0; i < 10000; i++) { + String p1 = paths.get(r.nextInt(paths.size())); + String p2 = paths.get(r.nextInt(paths.size())); + Filter.PathRestriction r1 = Filter.PathRestriction.values()[r + .nextInt(Filter.PathRestriction.values().length)]; + Filter.PathRestriction r2 = Filter.PathRestriction.values()[r + .nextInt(Filter.PathRestriction.values().length)]; + FilterImpl f1 = new FilterImpl(null); + f1.restrictPath(p1, r1); + FilterImpl f2 = new FilterImpl(null); + f2.restrictPath(p2, r2); + FilterImpl fc = new FilterImpl(null); + fc.restrictPath(p1, r1); + fc.restrictPath(p2, r2); + int tooMany = 0; + for (String p : paths) { + boolean expected = f1.testPath(p) && f2.testPath(p); + boolean got = fc.testPath(p); + if (expected == got) { + // good + } else if (expected && !got) { + fc = new FilterImpl(null); + fc.restrictPath(p1, r1); + fc.restrictPath(p2, r2); + fail("not matched: " + p1 + "/" + r1.name() + " && " + p2 + + "/" + r2.name()); + } else { + // not great, but not a problem + tooMany++; + } + } + if (tooMany > 3) { + fail("too many matches: " + p1 + "/" + r1.name() + " && " + p2 + + "/" + r2.name() + " superfluous: " + tooMany); + } + } + } + + @Test + public void pathRestrictions() throws Exception { + FilterImpl f = new FilterImpl(null); + assertEquals("/", f.getPath()); + assertEquals(Filter.PathRestriction.ALL_CHILDREN, + f.getPathRestriction()); + + f.restrictPath("/test", Filter.PathRestriction.ALL_CHILDREN); + f.restrictPath("/test2", Filter.PathRestriction.ALL_CHILDREN); + assertTrue(f.isAlwaysFalse()); + + f = new FilterImpl(null); + f.restrictPath("/test", Filter.PathRestriction.ALL_CHILDREN); + assertEquals("/test", f.getPath()); + assertEquals(Filter.PathRestriction.ALL_CHILDREN, + f.getPathRestriction()); + f.restrictPath("/test/x", Filter.PathRestriction.DIRECT_CHILDREN); + assertEquals("/test/x", f.getPath()); + assertEquals(Filter.PathRestriction.DIRECT_CHILDREN, + f.getPathRestriction()); + f.restrictPath("/test/x/y", Filter.PathRestriction.PARENT); + assertEquals("/test/x/y", f.getPath()); + assertEquals(Filter.PathRestriction.PARENT, f.getPathRestriction()); + + f = new FilterImpl(null); + f.restrictPath("/test", Filter.PathRestriction.DIRECT_CHILDREN); + f.restrictPath("/test/x/y", Filter.PathRestriction.PARENT); + assertEquals("/test/x/y", f.getPath()); + assertEquals(Filter.PathRestriction.PARENT, f.getPathRestriction()); + f.restrictPath("/test/y", Filter.PathRestriction.DIRECT_CHILDREN); + assertTrue(f.isAlwaysFalse()); + + f = new FilterImpl(null); + f.restrictPath("/test/x/y", Filter.PathRestriction.PARENT); + f.restrictPath("/test/x", Filter.PathRestriction.EXACT); + assertEquals("/test/x", f.getPath()); + assertEquals(Filter.PathRestriction.EXACT, f.getPathRestriction()); + f.restrictPath("/test/y", Filter.PathRestriction.EXACT); + assertTrue(f.isAlwaysFalse()); + + f = new FilterImpl(null); + f.restrictPath("/test", Filter.PathRestriction.ALL_CHILDREN); + f.restrictPath("/test", Filter.PathRestriction.PARENT); + assertTrue(f.isAlwaysFalse()); + + f = new FilterImpl(null); + f.restrictPath("/test/x", Filter.PathRestriction.PARENT); + f.restrictPath("/test", Filter.PathRestriction.ALL_CHILDREN); + assertEquals("/test/x", f.getPath()); + assertEquals(Filter.PathRestriction.PARENT, f.getPathRestriction()); + f.restrictPath("/test/x", Filter.PathRestriction.ALL_CHILDREN); + assertTrue(f.isAlwaysFalse()); + + f = new FilterImpl(null); + f.restrictPath("/test", Filter.PathRestriction.ALL_CHILDREN); + f.restrictPath("/test", Filter.PathRestriction.EXACT); + assertTrue(f.isAlwaysFalse()); + + f = new FilterImpl(null); + f.restrictPath("/test", Filter.PathRestriction.DIRECT_CHILDREN); + f.restrictPath("/test/x", Filter.PathRestriction.EXACT); + assertEquals("/test/x", f.getPath()); + assertEquals(Filter.PathRestriction.EXACT, f.getPathRestriction()); + + f = new FilterImpl(null); + f.restrictPath("/test", Filter.PathRestriction.DIRECT_CHILDREN); + f.restrictPath("/test/x/y", Filter.PathRestriction.EXACT); + assertTrue(f.isAlwaysFalse()); + + f = new FilterImpl(null); + f.restrictPath("/test/x", Filter.PathRestriction.PARENT); + f.restrictPath("/", Filter.PathRestriction.ALL_CHILDREN); + assertEquals("/test/x", f.getPath()); + assertEquals(Filter.PathRestriction.PARENT, f.getPathRestriction()); + f.restrictPath("/test/y", Filter.PathRestriction.EXACT); + assertTrue(f.isAlwaysFalse()); + + f = new FilterImpl(null); + f.restrictPath("/test", Filter.PathRestriction.DIRECT_CHILDREN); + assertEquals("/test", f.getPath()); + assertEquals(Filter.PathRestriction.DIRECT_CHILDREN, + f.getPathRestriction()); + f.restrictPath("/", Filter.PathRestriction.ALL_CHILDREN); + assertEquals("/test", f.getPath()); + assertEquals(Filter.PathRestriction.DIRECT_CHILDREN, + f.getPathRestriction()); + f.restrictPath("/test", Filter.PathRestriction.ALL_CHILDREN); + assertEquals("/test", f.getPath()); + assertEquals(Filter.PathRestriction.DIRECT_CHILDREN, + f.getPathRestriction()); + f.restrictPath("/test/x/y", Filter.PathRestriction.PARENT); + assertEquals("/test/x/y", f.getPath()); + assertEquals(Filter.PathRestriction.PARENT, f.getPathRestriction()); + f.restrictPath("/test2", Filter.PathRestriction.ALL_CHILDREN); + assertTrue(f.isAlwaysFalse()); + + f = new FilterImpl(null); + f.restrictPath("/test/x", Filter.PathRestriction.EXACT); + assertEquals("/test/x", f.getPath()); + assertEquals(Filter.PathRestriction.EXACT, f.getPathRestriction()); + f.restrictPath("/test", Filter.PathRestriction.ALL_CHILDREN); + f.restrictPath("/test", Filter.PathRestriction.DIRECT_CHILDREN); + f.restrictPath("/test/x/y", Filter.PathRestriction.PARENT); + f.restrictPath("/test/y", Filter.PathRestriction.DIRECT_CHILDREN); + assertTrue(f.isAlwaysFalse()); + + f = new FilterImpl(null); + f.restrictPath("/test/x/y", Filter.PathRestriction.PARENT); + assertEquals("/test/x/y", f.getPath()); + assertEquals(Filter.PathRestriction.PARENT, f.getPathRestriction()); + f.restrictPath("/test/x", Filter.PathRestriction.PARENT); + assertEquals("/test/x", f.getPath()); + assertEquals(Filter.PathRestriction.PARENT, f.getPathRestriction()); + f.restrictPath("/test", Filter.PathRestriction.ALL_CHILDREN); + assertEquals("/test/x", f.getPath()); + assertEquals(Filter.PathRestriction.PARENT, f.getPathRestriction()); + f.restrictPath("/test", Filter.PathRestriction.DIRECT_CHILDREN); + assertEquals("/test/x", f.getPath()); + assertEquals(Filter.PathRestriction.PARENT, f.getPathRestriction()); + f.restrictPath("/test/x", Filter.PathRestriction.PARENT); + assertEquals("/test/x", f.getPath()); + assertEquals(Filter.PathRestriction.PARENT, f.getPathRestriction()); + f.restrictPath("/test", Filter.PathRestriction.PARENT); + assertEquals("/test", f.getPath()); + assertEquals(Filter.PathRestriction.PARENT, f.getPathRestriction()); + f.restrictPath("/test2", Filter.PathRestriction.EXACT); + assertTrue(f.isAlwaysFalse()); + + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/index/TraversingIndexQueryTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/index/TraversingIndexQueryTest.java new file mode 100644 index 00000000000..aa1042e5152 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/index/TraversingIndexQueryTest.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.query.index; + +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.plugins.nodetype.InitialContent; +import org.apache.jackrabbit.oak.query.AbstractQueryTest; + +/** + * Tests the query engine using the default index implementation: the + * {@link TraversingIndex} + */ +public class TraversingIndexQueryTest extends AbstractQueryTest { + + @Override + protected ContentRepository createRepository() { + return new Oak() + .with(new InitialContent()) + .createContentRepository(); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/index/TraversingIndexTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/index/TraversingIndexTest.java new file mode 100644 index 00000000000..4f69ebc1654 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/index/TraversingIndexTest.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.query.index; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +import org.apache.jackrabbit.oak.kernel.KernelNodeState; +import org.apache.jackrabbit.oak.spi.query.Cursor; +import org.junit.Test; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +/** + * Tests the TraversingCursor. + */ +public class TraversingIndexTest { + + private final MicroKernel mk = new MicroKernelImpl(); + + private final LoadingCache cache = + CacheBuilder.newBuilder().build(new CacheLoader() { + @Override + public KernelNodeState load(String key) throws Exception { + int slash = key.indexOf('/'); + String revision = key.substring(0, slash); + String path = key.substring(slash); + // this method is strictly called _after_ the cache is initialized, + // when the fields are set + return new KernelNodeState(getMicroKernel(), path, revision, getCache()); + } + }); + + MicroKernel getMicroKernel() { + return mk; + } + + LoadingCache getCache() { + return cache; + } + + @Test + public void traverse() throws Exception { + TraversingIndex t = new TraversingIndex(); + + String head = mk.getHeadRevision(); + head = mk.commit("/", "+ \"parents\": { \"p0\": {\"id\": \"0\"}, \"p1\": {\"id\": \"1\"}, \"p2\": {\"id\": \"2\"}}", head, ""); + head = mk.commit("/", "+ \"children\": { \"c1\": {\"p\": \"1\"}, \"c2\": {\"p\": \"1\"}, \"c3\": {\"p\": \"2\"}, \"c4\": {\"p\": \"3\"}}", head, ""); + FilterImpl f = new FilterImpl(null); + + f.setPath("/"); + List paths = new ArrayList(); + Cursor c = t.query(f, new KernelNodeState(mk, "/", head, cache)); + while (c.next()) { + paths.add(c.currentRow().getPath()); + } + Collections.sort(paths); + assertEquals(Arrays.asList( + "/", "/children", "/children/c1", "/children/c2", + "/children/c3", "/children/c4", "/parents", + "/parents/p0", "/parents/p1", "/parents/p2"), + paths); + assertFalse(c.next()); + // endure it stays false + assertFalse(c.next()); + + f.setPath("/nowhere"); + c = t.query(f, new KernelNodeState(mk, "/", head, cache)); + assertFalse(c.next()); + // endure it stays false + assertFalse(c.next()); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/AbstractSecurityTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/AbstractSecurityTest.java new file mode 100644 index 00000000000..da95282a156 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/AbstractSecurityTest.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.security; + +import javax.jcr.Credentials; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.SimpleCredentials; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginException; + +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.plugins.nodetype.InitialContent; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +import org.junit.After; +import org.junit.Before; + +/** + * AbstractOakTest is the base class for oak test execution. + */ +public abstract class AbstractSecurityTest { + + private ContentRepository contentRepository; + + protected SecurityProvider securityProvider; + protected ContentSession admin; + + @Before + public void before() throws Exception { + contentRepository = new Oak() + .with(new InitialContent()) + .with(getSecurityProvider()) + .createContentRepository(); + + // TODO: OAK-17. workaround for missing test configuration + Configuration.setConfiguration(new OakConfiguration()); + admin = login(getAdminCredentials()); + + Configuration.setConfiguration(getConfiguration()); + } + + @After + public void after() throws Exception { + admin.close(); + Configuration.setConfiguration(null); + } + + protected SecurityProvider getSecurityProvider() { + if (securityProvider == null) { + securityProvider = new SecurityProviderImpl(); + } + return securityProvider; + } + protected Configuration getConfiguration() { + return new OakConfiguration(); + } + + protected ContentSession login(Credentials credentials) + throws LoginException, NoSuchWorkspaceException { + return contentRepository.login(credentials, null); + } + + protected Credentials getAdminCredentials() { + // TODO retrieve from config + return new SimpleCredentials("admin", "admin".toCharArray()); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/ConfigurationParametersTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/ConfigurationParametersTest.java new file mode 100644 index 00000000000..1d908448c2e --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/ConfigurationParametersTest.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; + +/** + * ConfigurationParametersTest... + */ +public class ConfigurationParametersTest { + + @Before + public void setup() {} + + @After + public void tearDown() {} + + @Test + public void testDefaultValue() { + TestObject testObject = new TestObject("t"); + Integer int1000 = new Integer(1000); + + ConfigurationParameters options = new ConfigurationParameters(); + + assertNull(options.getConfigValue("some", null)); + assertEquals(testObject, options.getConfigValue("some", testObject)); + assertEquals(int1000, options.getConfigValue("some", int1000)); + } + + @Test + public void testArrayDefaultValue() { + TestObject[] testArray = new TestObject[] {new TestObject("t")}; + + ConfigurationParameters options = new ConfigurationParameters(); + TestObject[] result = options.getConfigValue("test", new TestObject[0]); + assertNotNull(result); + assertEquals(0, result.length); + + result = options.getConfigValue("test", testArray); + assertEquals(result, testArray); + + options = new ConfigurationParameters(Collections.singletonMap("test", testArray)); + result = options.getConfigValue("test", null); + assertEquals(result, testArray); + } + + @Test + public void testConversion() { + TestObject testObject = new TestObject("t"); + Integer int1000 = new Integer(1000); + + Map m = new HashMap(); + m.put("TEST", testObject); + m.put("String", "1000"); + m.put("Int2", new Integer(1000)); + m.put("Int3", 1000); + + + ConfigurationParameters options = new ConfigurationParameters(m); + + assertNotNull(options.getConfigValue("TEST", null)); + assertEquals(testObject, options.getConfigValue("TEST", null)); + assertEquals(testObject, options.getConfigValue("TEST", testObject)); + assertEquals("t", options.getConfigValue("TEST", "defaultString")); + + assertEquals("1000", options.getConfigValue("String", null)); + assertEquals(int1000, options.getConfigValue("String", new Integer(10))); + assertEquals(new Long(1000), options.getConfigValue("String", new Long(10))); + assertEquals("1000", options.getConfigValue("String", "10")); + + assertEquals(int1000, options.getConfigValue("Int2", null)); + assertEquals(int1000, options.getConfigValue("Int2", new Integer(10))); + assertEquals("1000", options.getConfigValue("Int2", "1000")); + + assertEquals(1000, options.getConfigValue("Int3", null)); + assertEquals(int1000, options.getConfigValue("Int3", null)); + assertEquals(int1000, options.getConfigValue("Int3", new Integer(10))); + assertEquals("1000", options.getConfigValue("Int3", "1000")); + } + + + + private class TestObject { + + private final String name; + + private TestObject(String name) { + this.name = name; + } + + public String toString() { + return name; + } + + public boolean equals(Object object) { + if (object == this) { + return true; + } + if (object instanceof TestObject) { + return name.equals(((TestObject) object).name); + } + return false; + } + } +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/DefaultLoginModuleTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/DefaultLoginModuleTest.java new file mode 100644 index 00000000000..38626f6553a --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/DefaultLoginModuleTest.java @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authentication; + +import java.util.Collections; +import javax.jcr.GuestCredentials; +import javax.jcr.SimpleCredentials; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.api.AuthInfo; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.security.AbstractSecurityTest; +import org.apache.jackrabbit.oak.security.authentication.user.LoginModuleImpl; +import org.apache.jackrabbit.oak.spi.security.authentication.ImpersonationCredentials; +import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.apache.jackrabbit.oak.spi.security.user.util.UserUtility; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +/** + * LoginTest... + */ +public class DefaultLoginModuleTest extends AbstractSecurityTest { + + private UserConfiguration uc; + + @Before + public void before() throws Exception { + super.before(); + + uc = getSecurityProvider().getUserConfiguration(); + } + + @Override + protected Configuration getConfiguration() { + return new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String s) { + AppConfigurationEntry defaultEntry = new AppConfigurationEntry( + LoginModuleImpl.class.getName(), + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + Collections.emptyMap()); + + return new AppConfigurationEntry[] {defaultEntry}; + } + }; + } + + @Test + public void testNullLogin() throws Exception { + ContentSession cs = null; + try { + cs = login(null); + fail("Null login should fail"); + } catch (LoginException e) { + // success + } finally { + if (cs != null) { + cs.close(); + } + } + } + + @Test + public void testGuestLogin() throws Exception { + ContentSession cs = login(new GuestCredentials()); + try { + AuthInfo authInfo = cs.getAuthInfo(); + String anonymousID = UserUtility.getAnonymousId(uc.getConfigurationParameters()); + assertEquals(anonymousID, authInfo.getUserID()); + } finally { + cs.close(); + } + } + + @Test + public void testAnonymousLogin() throws Exception { + String anonymousID = UserUtility.getAnonymousId(uc.getConfigurationParameters()); + + Root root = admin.getLatestRoot(); + UserManager userMgr = uc.getUserManager(root, NamePathMapper.DEFAULT); + + // verify initial user-content looks like expected + Authorizable anonymous = userMgr.getAuthorizable(anonymousID); + assertNotNull(anonymous); + assertFalse(root.getTree(anonymous.getPath()).hasProperty(UserConstants.REP_PASSWORD)); + + ContentSession cs = null; + try { + cs = login(new SimpleCredentials(anonymousID, new char[0])); + fail("Login with anonymousID should fail since the initial setup doesn't provide a password."); + } catch (LoginException e) { + // success + } finally { + if (cs != null) { + cs.close(); + } + } + } + + @Test + public void testUserLogin() throws Exception { + Root root = admin.getLatestRoot(); + UserManager userManager = uc.getUserManager(root, NamePathMapper.DEFAULT); + + ContentSession cs = null; + User user = null; + try { + user = userManager.createUser("test", "pw"); + root.commit(); + + cs = login(new SimpleCredentials("test", "pw".toCharArray())); + AuthInfo authInfo = cs.getAuthInfo(); + assertEquals("test", authInfo.getUserID()); + } finally { + if (user != null) { + user.remove(); + root.commit(); + } + if (cs != null) { + cs.close(); + } + } + } + + @Test + public void testSelfImpersonation() throws Exception { + Root root = admin.getLatestRoot(); + UserManager userManager = uc.getUserManager(root, NamePathMapper.DEFAULT); + + ContentSession cs = null; + User user = null; + try { + user = userManager.createUser("test", "pw"); + root.commit(); + + SimpleCredentials sc = new SimpleCredentials("test", "pw".toCharArray()); + cs = login(sc); + + AuthInfo authInfo = cs.getAuthInfo(); + assertEquals("test", authInfo.getUserID()); + + cs.close(); + + sc = new SimpleCredentials("test", new char[0]); + ImpersonationCredentials ic = new ImpersonationCredentials(sc, authInfo); + cs = login(ic); + + authInfo = cs.getAuthInfo(); + assertEquals("test", authInfo.getUserID()); + } finally { + if (user != null) { + user.remove(); + root.commit(); + } + if (cs != null) { + cs.close(); + } + } + } + + @Test + public void testInvalidImpersonation() throws Exception { + Root root = admin.getLatestRoot(); + UserManager userManager = uc.getUserManager(root, NamePathMapper.DEFAULT); + + ContentSession cs = null; + User user = null; + try { + user = userManager.createUser("test", "pw"); + root.commit(); + + SimpleCredentials sc = new SimpleCredentials("test", "pw".toCharArray()); + cs = login(sc); + + AuthInfo authInfo = cs.getAuthInfo(); + assertEquals("test", authInfo.getUserID()); + + cs.close(); + cs = null; + + String adminId = UserUtility.getAdminId(securityProvider.getUserConfiguration().getConfigurationParameters()); + sc = new SimpleCredentials(adminId, new char[0]); + ImpersonationCredentials ic = new ImpersonationCredentials(sc, authInfo); + + try { + cs = login(ic); + fail("User 'test' should not be allowed to impersonate " + adminId); + } catch (LoginException e) { + // success + } + } finally { + if (user != null) { + user.remove(); + root.commit(); + } + if (cs != null) { + cs.close(); + } + } + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/GuestDefaultLoginModuleTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/GuestDefaultLoginModuleTest.java new file mode 100644 index 00000000000..e15f1653121 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/GuestDefaultLoginModuleTest.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authentication; + +import java.util.Collections; +import javax.jcr.GuestCredentials; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; + +import org.apache.jackrabbit.oak.api.AuthInfo; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.security.AbstractSecurityTest; +import org.apache.jackrabbit.oak.security.authentication.user.LoginModuleImpl; +import org.apache.jackrabbit.oak.spi.security.authentication.GuestLoginModule; +import org.apache.jackrabbit.oak.spi.security.user.util.UserUtility; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * LoginTest... + */ +public class GuestDefaultLoginModuleTest extends AbstractSecurityTest { + + @Override + protected Configuration getConfiguration() { + return new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String s) { + AppConfigurationEntry guestEntry = new AppConfigurationEntry( + GuestLoginModule.class.getName(), + AppConfigurationEntry.LoginModuleControlFlag.OPTIONAL, + Collections.emptyMap()); + + AppConfigurationEntry defaultEntry = new AppConfigurationEntry( + LoginModuleImpl.class.getName(), + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + Collections.emptyMap()); + + return new AppConfigurationEntry[] {guestEntry, defaultEntry}; + } + }; + } + + @Test + public void testNullLogin() throws Exception { + ContentSession cs = login(null); + try { + AuthInfo authInfo = cs.getAuthInfo(); + String anonymousID = UserUtility.getAnonymousId(getSecurityProvider().getUserConfiguration().getConfigurationParameters()); + assertEquals(anonymousID, authInfo.getUserID()); + } finally { + cs.close(); + } + } + + @Test + public void testGuestLogin() throws Exception { + ContentSession cs = login(new GuestCredentials()); + try { + AuthInfo authInfo = cs.getAuthInfo(); + String anonymousID = UserUtility.getAnonymousId(getSecurityProvider().getUserConfiguration().getConfigurationParameters()); + assertEquals(anonymousID, authInfo.getUserID()); + } finally { + cs.close(); + } + } +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/TokenDefaultLoginModuleTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/TokenDefaultLoginModuleTest.java new file mode 100644 index 00000000000..d9ff63218d3 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/TokenDefaultLoginModuleTest.java @@ -0,0 +1,202 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authentication; + +import java.util.Collections; +import javax.jcr.GuestCredentials; +import javax.jcr.SimpleCredentials; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.security.AbstractSecurityTest; +import org.apache.jackrabbit.oak.security.authentication.token.TokenLoginModule; +import org.apache.jackrabbit.oak.security.authentication.user.LoginModuleImpl; +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenInfo; +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenProvider; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +/** + * TokenDefaultLoginModuleTest... + */ +public class TokenDefaultLoginModuleTest extends AbstractSecurityTest { + + @Override + protected Configuration getConfiguration() { + return new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String s) { + AppConfigurationEntry tokenEntry = new AppConfigurationEntry( + TokenLoginModule.class.getName(), + AppConfigurationEntry.LoginModuleControlFlag.SUFFICIENT, + Collections.emptyMap()); + + AppConfigurationEntry defaultEntry = new AppConfigurationEntry( + LoginModuleImpl.class.getName(), + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + Collections.emptyMap()); + return new AppConfigurationEntry[] {tokenEntry, defaultEntry}; + } + }; + } + + @Test + public void testNullLogin() throws Exception { + ContentSession cs = null; + try { + cs = login(null); + fail("Null login should fail"); + } catch (LoginException e) { + // success + } finally { + if (cs != null) { + cs.close(); + } + } + } + + @Test + public void testGuestLogin() throws Exception { + ContentSession cs = null; + try { + cs = login(new GuestCredentials()); + } finally { + if (cs != null) { + cs.close(); + } + } + } + + @Test + public void testInvalidSimpleCredentials() throws Exception { + ContentSession cs = null; + try { + SimpleCredentials sc = new SimpleCredentials("test", new char[0]); + cs = login(sc); + fail("Invalid simple credentials login should fail"); + } catch (LoginException e) { + // success + } finally { + if (cs != null) { + cs.close(); + } + } + } + + @Test + public void testInvalidSimpleCredentialsWithAttribute() throws Exception { + ContentSession cs = null; + try { + SimpleCredentials sc = new SimpleCredentials("test", new char[0]); + sc.setAttribute(".token", ""); + + cs = login(sc); + fail("Invalid simple credentials login should fail"); + } catch (LoginException e) { + // success + } finally { + if (cs != null) { + cs.close(); + } + } + } + + @Test + public void testSimpleCredentials() throws Exception { + ContentSession cs = null; + try { + cs = login(getAdminCredentials()); + } finally { + if (cs != null) { + cs.close(); + } + } + } + + @Test + public void testSimpleCredentialsWithAttribute() throws Exception { + ContentSession cs = null; + try { + SimpleCredentials sc = (SimpleCredentials) getAdminCredentials(); + sc.setAttribute(".token", ""); + cs = login(sc); + } finally { + if (cs != null) { + cs.close(); + } + } + } + + @Test + public void testTokenCreationAndLogin() throws Exception { + ContentSession cs = null; + try { + SimpleCredentials sc = (SimpleCredentials) getAdminCredentials(); + sc.setAttribute(".token", ""); + cs = login(sc); + + Object token = sc.getAttribute(".token").toString(); + assertNotNull(token); + TokenCredentials tc = new TokenCredentials(token.toString()); + + cs.close(); + cs = login(tc); + } finally { + if (cs != null) { + cs.close(); + } + } + } + + @Test + public void testInvalidTokenCredentials() throws Exception { + ContentSession cs = null; + try { + cs = login(new TokenCredentials("invalid")); + fail("Invalid token credentials login should fail"); + } catch (LoginException e) { + // success + } finally { + if (cs != null) { + cs.close(); + } + } + } + + @Test + public void testValidTokenCredentials() throws Exception { + Root root = admin.getLatestRoot(); + TokenProvider tp = getSecurityProvider().getTokenProvider(root); + + SimpleCredentials sc = (SimpleCredentials) getAdminCredentials(); + TokenInfo info = tp.createToken(sc.getUserID(), Collections.emptyMap()); + + ContentSession cs = login(new TokenCredentials(info.getToken())); + try { + assertEquals(sc.getUserID(), cs.getAuthInfo().getUserID()); + } finally { + cs.close(); + } + } +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/TokenLoginModuleTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/TokenLoginModuleTest.java new file mode 100644 index 00000000000..68fcc5e3987 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/TokenLoginModuleTest.java @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authentication; + +import java.util.Collections; +import javax.jcr.GuestCredentials; +import javax.jcr.SimpleCredentials; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.security.AbstractSecurityTest; +import org.apache.jackrabbit.oak.security.authentication.token.TokenLoginModule; +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenInfo; +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenProvider; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * TokenLoginModuleTest... + */ +public class TokenLoginModuleTest extends AbstractSecurityTest { + + @Override + protected Configuration getConfiguration() { + return new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String s) { + AppConfigurationEntry defaultEntry = new AppConfigurationEntry( + TokenLoginModule.class.getName(), + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + Collections.emptyMap()); + + return new AppConfigurationEntry[] {defaultEntry}; + } + }; + } + + @Test + public void testNullLogin() throws Exception { + ContentSession cs = null; + try { + cs = login(null); + fail("Null login should fail"); + } catch (LoginException e) { + // success + } finally { + if (cs != null) { + cs.close(); + } + } + } + + @Test + public void testGuestLogin() throws Exception { + ContentSession cs = null; + try { + cs = login(new GuestCredentials()); + fail("GuestCredentials login should fail"); + } catch (LoginException e) { + // success + } finally { + if (cs != null) { + cs.close(); + } + } + } + + @Test + public void testSimpleCredentials() throws Exception { + ContentSession cs = null; + try { + SimpleCredentials sc = new SimpleCredentials("admin", "admin".toCharArray()); + cs = login(sc); + fail("Unsupported credentials login should fail"); + } catch (LoginException e) { + // success + } finally { + if (cs != null) { + cs.close(); + } + } + } + + @Test + public void testSimpleCredentialsWithAttribute() throws Exception { + ContentSession cs = null; + try { + SimpleCredentials sc = new SimpleCredentials("test", new char[0]); + sc.setAttribute(".token", ""); + + cs = login(sc); + fail("Unsupported credentials login should fail"); + } catch (LoginException e) { + // success + } finally { + if (cs != null) { + cs.close(); + } + } + } + + @Test + public void testInvalidTokenCredentials() throws Exception { + ContentSession cs = null; + try { + cs = login(new TokenCredentials("invalid")); + fail("Invalid token credentials login should fail"); + } catch (LoginException e) { + // success + } finally { + if (cs != null) { + cs.close(); + } + } + } + + @Test + public void testValidTokenCredentials() throws Exception { + Root root = admin.getLatestRoot(); + TokenProvider tp = getSecurityProvider().getTokenProvider(root); + + SimpleCredentials sc = (SimpleCredentials) getAdminCredentials(); + TokenInfo info = tp.createToken(sc.getUserID(), Collections.emptyMap()); + + ContentSession cs = login(new TokenCredentials(info.getToken())); + try { + assertEquals(sc.getUserID(), cs.getAuthInfo().getUserID()); + } finally { + cs.close(); + } + } +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/token/AbstractTokenTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/token/AbstractTokenTest.java new file mode 100644 index 00000000000..be50ab02a91 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/token/AbstractTokenTest.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.security.authentication.token; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.security.AbstractSecurityTest; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.junit.After; +import org.junit.Before; + +/** + * AbstractTokenTest... + */ +public abstract class AbstractTokenTest extends AbstractSecurityTest { + + Root root; + TokenProviderImpl tokenProvider; + + String userId; + UserManager userManager; + + @Before + public void before() throws Exception { + super.before(); + + root = admin.getLatestRoot(); + tokenProvider = new TokenProviderImpl(root, + ConfigurationParameters.EMPTY, + getSecurityProvider().getUserConfiguration()); + + userId = "testUser"; + userManager = getSecurityProvider().getUserConfiguration().getUserManager(root, NamePathMapper.DEFAULT); + + userManager.createUser(userId, "pw"); + root.commit(); + } + + @After + public void after() throws Exception { + try { + Authorizable a = userManager.getAuthorizable(userId); + if (a != null) { + a.remove(); + root.commit(); + } + } finally { + super.after(); + } + } +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/token/TokenAuthenticationTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/token/TokenAuthenticationTest.java new file mode 100644 index 00000000000..59e307ad20a --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/token/TokenAuthenticationTest.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.security.authentication.token; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import javax.jcr.Credentials; +import javax.jcr.GuestCredentials; +import javax.jcr.SimpleCredentials; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; +import org.apache.jackrabbit.oak.spi.security.authentication.Authentication; +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenInfo; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * TokenAuthenticationTest... + */ +public class TokenAuthenticationTest extends AbstractTokenTest { + + TokenAuthentication authentication; + + @Before + public void before() throws Exception { + super.before(); + authentication = new TokenAuthentication(tokenProvider); + } + @Test + public void testAuthenticateWithoutTokenProvider() throws Exception { + Authentication authentication = new TokenAuthentication(null); + + assertFalse(authentication.authenticate(new TokenCredentials("token"))); + } + + @Test + public void testAuthenticateWithInvalidCredentials() throws Exception { + List invalid = new ArrayList(); + invalid.add(new GuestCredentials()); + invalid.add(new SimpleCredentials(userId, new char[0])); + + for (Credentials creds : invalid) { + assertFalse(authentication.authenticate(creds)); + } + } + + @Test + public void testAuthenticateWithInvalidTokenCredentials() throws Exception { + try { + authentication.authenticate(new TokenCredentials(UUID.randomUUID().toString())); + fail("LoginException expected"); + } catch (LoginException e) { + // success + } + } + + @Test + public void testAuthenticate() throws Exception { + TokenInfo info = tokenProvider.createToken(userId, Collections.emptyMap()); + assertTrue(authentication.authenticate(new TokenCredentials(info.getToken()))); + } + + @Test + public void testGetTokenInfoBeforeAuthenticate() { + try { + authentication.getTokenInfo(); + fail("IllegalStateException expected"); + } catch (IllegalStateException e) { + // success + } + } + + @Test + public void testGetTokenInfoAfterAuthenticate() throws Exception { + TokenInfo info = tokenProvider.createToken(userId, Collections.emptyMap()); + authentication.authenticate(new TokenCredentials(info.getToken())); + + TokenInfo info2 = authentication.getTokenInfo(); + assertNotNull(info2); + assertEquals(info.getUserId(), info2.getUserId()); + } +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/token/TokenInfoTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/token/TokenInfoTest.java new file mode 100644 index 00000000000..624f18bae7b --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/token/TokenInfoTest.java @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authentication.token; + +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenInfo; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * TokenInfoTest... + */ +public class TokenInfoTest extends AbstractTokenTest { + + @Test + public void testGetUserId() { + TokenInfo info = tokenProvider.createToken(userId, Collections.emptyMap()); + assertEquals(userId, info.getUserId()); + + info = tokenProvider.getTokenInfo(info.getToken()); + assertEquals(userId, info.getUserId()); + } + + @Test + public void testGetToken() { + TokenInfo info = tokenProvider.createToken(userId, Collections.emptyMap()); + assertNotNull(info.getToken()); + + info = tokenProvider.getTokenInfo(info.getToken()); + assertNotNull(info.getToken()); + } + + @Test + public void testIsExpired() { + long loginTime = new Date().getTime(); + + TokenInfo info = tokenProvider.createToken(userId, Collections.emptyMap()); + assertFalse(info.isExpired(loginTime)); + + loginTime = new Date().getTime() + 3600000; + assertFalse(info.isExpired(loginTime)); + + long expiredTime = new Date().getTime() + 7200001; + assertTrue(info.isExpired(expiredTime)); + } + + @Test + public void testMatches() { + TokenInfo info = tokenProvider.createToken(userId, Collections.emptyMap()); + assertTrue(info.matches(new TokenCredentials(info.getToken()))); + + Map attributes = new HashMap(); + attributes.put("something", "value"); + info = tokenProvider.createToken(userId, attributes); + assertTrue(info.matches(new TokenCredentials(info.getToken()))); + + attributes.put(".token-something", "mandatory"); + info = tokenProvider.createToken(userId, attributes); + assertFalse(info.matches(new TokenCredentials(info.getToken()))); + TokenCredentials tc = new TokenCredentials(info.getToken()); + tc.setAttribute(".token-something", "mandatory"); + assertTrue(info.matches(tc)); + tc.setAttribute("another", "value"); + assertTrue(info.matches(tc)); + tc.setAttribute(".token_ignored", "value"); + assertTrue(info.matches(tc)); + } + + @Test + public void testGetAttributes() { + Map reserved = new HashMap(); + reserved.put(".token", "value"); + reserved.put("rep:token.key", "value"); + reserved.put("rep:token.exp", "value"); + + Map privateAttributes = new HashMap(); + privateAttributes.put(".token_exp", "value"); + privateAttributes.put(".tokenTest", "value"); + privateAttributes.put(".token_something", "value"); + + Map publicAttributes = new HashMap(); + publicAttributes.put("any", "value"); + publicAttributes.put("another", "value"); + + Map attributes = new HashMap(); + attributes.putAll(reserved); + attributes.putAll(publicAttributes); + attributes.putAll(privateAttributes); + + TokenInfo info = tokenProvider.createToken(userId, attributes); + + Map pubAttr = info.getPublicAttributes(); + assertEquals("public attributes",publicAttributes.size(), pubAttr.size()); + for (String key : publicAttributes.keySet()) { + assertTrue("public attribute "+key+" not contained",pubAttr.containsKey(key)); + assertEquals("public attribute " + key,publicAttributes.get(key), pubAttr.get(key)); + } + + Map privAttr = info.getPrivateAttributes(); + assertEquals("private attributes",privateAttributes.size(), privAttr.size()); + for (String key : privateAttributes.keySet()) { + assertTrue("private attribute "+key+" not contained",privAttr.containsKey(key)); + assertEquals("private attribute" + key,privateAttributes.get(key), privAttr.get(key)); + } + + for (String key : reserved.keySet()) { + assertFalse("reserved attribute "+key,privAttr.containsKey(key)); + assertFalse("reserved attribute "+key,pubAttr.containsKey(key)); + } + } +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProviderImplTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProviderImplTest.java new file mode 100644 index 00000000000..f9b91d2dba7 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProviderImplTest.java @@ -0,0 +1,275 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authentication.token; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.jcr.Credentials; +import javax.jcr.GuestCredentials; +import javax.jcr.SimpleCredentials; + +import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; +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.spi.security.authentication.ImpersonationCredentials; +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenInfo; +import org.junit.Test; + +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; + +/** + * TokenProviderImplTest... + */ +public class TokenProviderImplTest extends AbstractTokenTest { + + @Test + public void testDoCreateToken() throws Exception { + assertFalse(tokenProvider.doCreateToken(new GuestCredentials())); + assertFalse(tokenProvider.doCreateToken(new TokenCredentials("token"))); + assertFalse(tokenProvider.doCreateToken(getAdminCredentials())); + + SimpleCredentials sc = new SimpleCredentials("uid", "pw".toCharArray()); + assertFalse(tokenProvider.doCreateToken(sc)); + + sc.setAttribute("any_attribute", "value"); + assertFalse(tokenProvider.doCreateToken(sc)); + + sc.setAttribute("rep:token_key", "value"); + assertFalse(tokenProvider.doCreateToken(sc)); + + sc.setAttribute(".token", "existing"); + assertFalse(tokenProvider.doCreateToken(sc)); + + sc.setAttribute(".token", ""); + assertTrue(tokenProvider.doCreateToken(sc)); + } + + @Test + public void testCreateTokenFromInvalidCredentials() throws Exception { + List invalid = new ArrayList(); + invalid.add(new GuestCredentials()); + invalid.add(new TokenCredentials("sometoken")); + invalid.add(new ImpersonationCredentials(new GuestCredentials(), null)); + invalid.add(new SimpleCredentials("unknownUserId", new char[0])); + + for (Credentials creds : invalid) { + assertNull(tokenProvider.createToken(creds)); + } + } + + @Test + public void testCreateTokenFromCredentials() throws Exception { + SimpleCredentials sc = new SimpleCredentials(userId, new char[0]); + List valid = new ArrayList(); + valid.add(sc); + valid.add(new ImpersonationCredentials(sc, null)); + + for (Credentials creds : valid) { + TokenInfo info = tokenProvider.createToken(creds); + assertTokenInfo(info, userId); + } + } + + @Test + public void testCreateTokenFromInvalidUserId() throws Exception { + TokenInfo info = tokenProvider.createToken("unknownUserId", Collections.emptyMap()); + assertNull(info); + } + + @Test + public void testCreateTokenFromUserId() throws Exception { + TokenInfo info = tokenProvider.createToken(userId, Collections.emptyMap()); + assertTokenInfo(info, userId); + } + + @Test + public void testTokenNode() throws Exception { + Map reserved = new HashMap(); + reserved.put(".token", "value"); + reserved.put("rep:token.key", "value"); + reserved.put("rep:token.exp", "value"); + + Map privateAttributes = new HashMap(); + privateAttributes.put(".token_exp", "value"); + privateAttributes.put(".tokenTest", "value"); + privateAttributes.put(".token_something", "value"); + + Map publicAttributes = new HashMap(); + publicAttributes.put("any", "value"); + publicAttributes.put("another", "value"); + + Map attributes = new HashMap(); + attributes.putAll(reserved); + attributes.putAll(publicAttributes); + attributes.putAll(privateAttributes); + + TokenInfo info = tokenProvider.createToken(userId, attributes); + + Tree userTree = root.getTree(userManager.getAuthorizable(userId).getPath()); + Tree tokens = userTree.getChild(".tokens"); + assertNotNull(tokens); + assertEquals(1, tokens.getChildrenCount()); + + Tree tokenNode = tokens.getChildren().iterator().next(); + assertNotNull(tokenNode.getProperty("rep:token.key")); + assertNotNull(tokenNode.getProperty("rep:token.exp")); + + for (String key : reserved.keySet()) { + PropertyState p = tokenNode.getProperty(key); + if (p != null) { + assertFalse(reserved.get(key).equals(p.getValue(Type.STRING))); + } + } + + for (String key : privateAttributes.keySet()) { + assertEquals(privateAttributes.get(key), tokenNode.getProperty(key).getValue(Type.STRING)); + } + + for (String key : publicAttributes.keySet()) { + assertEquals(publicAttributes.get(key), tokenNode.getProperty(key).getValue(Type.STRING)); + } + } + + @Test + public void testGetTokenInfoFromInvalidToken() throws Exception { + List invalid = new ArrayList(); + invalid.add("/invalid"); + invalid.add(UUID.randomUUID().toString()); + + for (String token : invalid) { + TokenInfo info = tokenProvider.getTokenInfo(token); + assertNull(info); + } + + try { + assertNull(tokenProvider.getTokenInfo("invalidToken")); + } catch (Exception e) { + // success + } + } + + @Test + public void testGetTokenInfo() throws Exception { + String token = tokenProvider.createToken(userId, Collections.emptyMap()).getToken(); + TokenInfo info = tokenProvider.getTokenInfo(token); + assertTokenInfo(info, userId); + } + + @Test + public void testRemoveTokenInvalidInfo() throws Exception { + assertFalse(tokenProvider.removeToken(new InvalidTokenInfo())); + } + + @Test + public void testRemoveToken() throws Exception { + TokenInfo info = tokenProvider.createToken(userId, Collections.emptyMap()); + assertTrue(tokenProvider.removeToken(info)); + } + + @Test + public void testRemoveToken2() throws Exception { + TokenInfo info = tokenProvider.createToken(userId, Collections.emptyMap()); + assertTrue(tokenProvider.removeToken(tokenProvider.getTokenInfo(info.getToken()))); + } + + @Test + public void testRemoveTokenRemovesNode() throws Exception { + TokenInfo info = tokenProvider.createToken(userId, Collections.emptyMap()); + + Tree userTree = root.getTree(userManager.getAuthorizable(userId).getPath()); + Tree tokens = userTree.getChild(".tokens"); + String tokenNodePath = tokens.getChildren().iterator().next().getPath(); + + tokenProvider.removeToken(info); + assertNull(root.getTree(tokenNodePath)); + } + + @Test + public void testResetTokenExpirationInvalidToken() throws Exception { + assertFalse(tokenProvider.resetTokenExpiration(new InvalidTokenInfo(), new Date().getTime())); + } + + @Test + public void testResetTokenExpirationExpiredToken() throws Exception { + TokenInfo info = tokenProvider.createToken(userId, Collections.emptyMap()); + + long expiredTime = new Date().getTime() + 7200001; + assertTrue(info.isExpired(expiredTime)); + assertFalse(tokenProvider.resetTokenExpiration(info, expiredTime)); + } + + @Test + public void testResetTokenExpiration() throws Exception { + TokenInfo info = tokenProvider.createToken(userId, Collections.emptyMap()); + + assertFalse(tokenProvider.resetTokenExpiration(info, new Date().getTime())); + + long loginTime = new Date().getTime() + 3600000; + assertFalse(info.isExpired(loginTime)); + assertTrue(tokenProvider.resetTokenExpiration(info, loginTime)); + } + + //-------------------------------------------------------------------------- + private static void assertTokenInfo(TokenInfo info, String userId) { + assertNotNull(info); + assertNotNull(info.getToken()); + assertEquals(userId, info.getUserId()); + assertFalse(info.isExpired(new Date().getTime())); + } + + private final class InvalidTokenInfo implements TokenInfo { + @Nonnull + @Override + public String getUserId() { + return "invalid"; + } + @Nonnull + @Override + public String getToken() { + return "invalid"; + } + @Override + public boolean isExpired(long loginTime) { + return true; + } + @Override + public boolean matches(TokenCredentials tokenCredentials) { + return false; + } + @Nonnull + @Override + public Map getPrivateAttributes() { + return null; + } + @Nonnull + @Override + public Map getPublicAttributes() { + return null; + } + } +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/user/UserAuthenticationTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/user/UserAuthenticationTest.java new file mode 100644 index 00000000000..310d66b4708 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/user/UserAuthenticationTest.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.security.authentication.user; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.jcr.Credentials; +import javax.jcr.GuestCredentials; +import javax.jcr.SimpleCredentials; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.api.AuthInfo; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.security.AbstractSecurityTest; +import org.apache.jackrabbit.oak.spi.security.authentication.ImpersonationCredentials; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * UserAuthenticationTest... + */ +public class UserAuthenticationTest extends AbstractSecurityTest { + + private Root root; + + private String userId; + private UserManager userManager; + + private UserAuthentication authentication; + + @Before + public void before() throws Exception { + super.before(); + + root = admin.getLatestRoot(); + + userId = "testUser"; + userManager = getSecurityProvider().getUserConfiguration().getUserManager(root, NamePathMapper.DEFAULT); + + userManager.createUser(userId, "pw"); + root.commit(); + + authentication = new UserAuthentication(userId, userManager); + } + + @After + public void after() throws Exception { + try { + Authorizable a = userManager.getAuthorizable(userId); + if (a != null) { + a.remove(); + root.commit(); + } + } finally { + super.after(); + } + } + + @Test + public void testAuthenticateWithoutUserManager() throws Exception { + UserAuthentication authentication = new UserAuthentication(userId, null); + assertFalse(authentication.authenticate(new SimpleCredentials(userId, "pw".toCharArray()))); + } + + @Test + public void testAuthenticateWithoutUserId() throws Exception { + UserAuthentication authentication = new UserAuthentication(null, userManager); + assertFalse(authentication.authenticate(new SimpleCredentials(userId, "pw".toCharArray()))); + } + + @Test + public void testAuthenticateInvalidCredentials() throws Exception { + List invalid = new ArrayList(); + invalid.add(new TokenCredentials("token")); + invalid.add(new Credentials() {}); + + for (Credentials creds : invalid) { + assertFalse(authentication.authenticate(creds)); + } + } + + @Test + public void testAuthenticateInvalidSimpleCredentials() throws Exception { + List invalid = new ArrayList(); + invalid.add(new SimpleCredentials(userId, "wrongPw".toCharArray())); + invalid.add(new SimpleCredentials(userId, "".toCharArray())); + invalid.add(new SimpleCredentials("unknownUser", "pw".toCharArray())); + + for (Credentials creds : invalid) { + try { + authentication.authenticate(creds); + fail("LoginException expected"); + } catch (LoginException e) { + // success + } + } + } + + @Test + public void testAuthenticateSimpleCredentials() throws Exception { + assertTrue(authentication.authenticate(new SimpleCredentials(userId, "pw".toCharArray()))); + } + + @Test + public void testAuthenticateInvalidImpersonationCredentials() throws Exception { + List invalid = new ArrayList(); + invalid.add(new ImpersonationCredentials(new GuestCredentials(), admin.getAuthInfo())); + invalid.add(new ImpersonationCredentials(new SimpleCredentials(admin.getAuthInfo().getUserID(), new char[0]), new TestAuthInfo())); + invalid.add(new ImpersonationCredentials(new SimpleCredentials("unknown", new char[0]), admin.getAuthInfo())); + invalid.add(new ImpersonationCredentials(new SimpleCredentials("unknown", new char[0]), new TestAuthInfo())); + + for (Credentials creds : invalid) { + try { + authentication.authenticate(creds); + fail("LoginException expected"); + } catch (LoginException e) { + // success + } + } + } + + @Test + public void testAuthenticateImpersonationCredentials() throws Exception { + SimpleCredentials sc = new SimpleCredentials(userId, new char[0]); + assertTrue(authentication.authenticate(new ImpersonationCredentials(sc, admin.getAuthInfo()))); + } + + @Test + public void testAuthenticateImpersonationCredentials2() throws Exception { + SimpleCredentials sc = new SimpleCredentials(userId, new char[0]); + assertTrue(authentication.authenticate(new ImpersonationCredentials(sc, new TestAuthInfo()))); + } + + //-------------------------------------------------------------------------- + + private final class TestAuthInfo implements AuthInfo { + + @Override + public String getUserID() { + return userId; + } + @Nonnull + @Override + public String[] getAttributeNames() { + return new String[0]; + } + @Override + public Object getAttribute(String attributeName) { + return null; + } + @Override + public Set getPrincipals() { + return null; + } + } +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/principal/PrincipalProviderImplTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/principal/PrincipalProviderImplTest.java new file mode 100644 index 00000000000..24bac077113 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/principal/PrincipalProviderImplTest.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.principal; + +import java.security.Principal; +import java.util.Set; + +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.security.AbstractSecurityTest; +import org.apache.jackrabbit.oak.spi.security.principal.AdminPrincipal; +import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * PrincipalProviderImplTest... + */ +public class PrincipalProviderImplTest extends AbstractSecurityTest { + + @Test + public void testGetPrincipals() throws Exception { + Root root = admin.getLatestRoot(); + PrincipalProviderImpl principalProvider = + new PrincipalProviderImpl(root, getSecurityProvider().getUserConfiguration(), NamePathMapper.DEFAULT); + + String adminId = admin.getAuthInfo().getUserID(); + Set principals = principalProvider.getPrincipals(adminId); + + assertNotNull(principals); + assertFalse(principals.isEmpty()); + assertTrue(principals.contains(EveryonePrincipal.getInstance())); + + boolean containsAdminPrincipal = false; + for (Principal principal : principals) { + assertNotNull(principalProvider.getPrincipal(principal.getName())); + if (principal instanceof AdminPrincipal) { + containsAdminPrincipal = true; + } + } + assertTrue(containsAdminPrincipal); + } + + @Test + public void testEveryone() throws Exception { + Root root = admin.getLatestRoot(); + UserConfiguration config = getSecurityProvider().getUserConfiguration(); + + PrincipalProviderImpl principalProvider = new PrincipalProviderImpl(root, config, NamePathMapper.DEFAULT); + + Principal everyone = principalProvider.getPrincipal(EveryonePrincipal.NAME); + assertTrue(everyone instanceof EveryonePrincipal); + + org.apache.jackrabbit.api.security.user.Group everyoneGroup = null; + try { + UserManager userMgr = config.getUserManager(root, NamePathMapper.DEFAULT); + everyoneGroup = userMgr.createGroup(EveryonePrincipal.NAME); + root.commit(); + + Principal ep = principalProvider.getPrincipal(EveryonePrincipal.NAME); + assertFalse(ep instanceof EveryonePrincipal); + } finally { + if (everyoneGroup != null) { + everyoneGroup.remove(); + root.commit(); + } + } + } +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserManagerImplTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserManagerImplTest.java new file mode 100644 index 00000000000..5e851ba3c9e --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserManagerImplTest.java @@ -0,0 +1,234 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.oak.api.CommitFailedException; +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.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.security.AbstractSecurityTest; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.apache.jackrabbit.oak.spi.security.user.util.PasswordUtility; +import org.apache.jackrabbit.oak.util.NodeUtil; +import org.junit.Before; +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +/** + * UserManagerImplTest... + */ +public class UserManagerImplTest extends AbstractSecurityTest { + + private Root root; + private UserManagerImpl userMgr; + + @Before + public void before() throws Exception { + super.before(); + + root = admin.getLatestRoot(); + userMgr = new UserManagerImpl(null, root, NamePathMapper.DEFAULT, getSecurityProvider()); + } + + @Test + public void testSetPassword() throws Exception { + User user = userMgr.createUser("a", "pw"); + root.commit(); + + List pwds = new ArrayList(); + pwds.add("pw"); + pwds.add(""); + pwds.add("{sha1}pw"); + + Tree userTree = root.getTree(user.getPath()); + for (String pw : pwds) { + userMgr.setPassword(userTree, pw, true); + String pwHash = userTree.getProperty(UserConstants.REP_PASSWORD).getValue(Type.STRING); + assertNotNull(pwHash); + assertTrue(PasswordUtility.isSame(pwHash, pw)); + } + + for (String pw : pwds) { + userMgr.setPassword(userTree, pw, false); + String pwHash = userTree.getProperty(UserConstants.REP_PASSWORD).getValue(Type.STRING); + assertNotNull(pwHash); + if (!pw.startsWith("{")) { + assertTrue(PasswordUtility.isSame(pwHash, pw)); + } else { + assertFalse(PasswordUtility.isSame(pwHash, pw)); + assertEquals(pw, pwHash); + } + } + } + + @Test + public void setPasswordNull() throws Exception { + User user = userMgr.createUser("a", null); + root.commit(); + + Tree userTree = root.getTree(user.getPath()); + try { + userMgr.setPassword(userTree, null, true); + fail("setting null password should fail"); + } catch (IllegalArgumentException e) { + // expected + } + + try { + userMgr.setPassword(userTree, null, false); + fail("setting null password should fail"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testGetPasswordHash() throws Exception { + User user = userMgr.createUser("a", null); + root.commit(); + + Tree userTree = root.getTree(user.getPath()); + assertNull(userTree.getProperty(UserConstants.REP_PASSWORD)); + } + + @Test + public void testIsAutoSave() throws Exception { + assertFalse(userMgr.isAutoSave()); + } + + @Test + public void testAutoSave() throws Exception { + try { + userMgr.autoSave(true); + fail("should fail"); + } catch (UnsupportedRepositoryOperationException e) { + // success + } + } + + @Test + public void testEnforceAuthorizableFolderHierarchy() throws RepositoryException, CommitFailedException { + User user = userMgr.createUser("testUser", null); + root.commit(); + + NodeUtil userNode = new NodeUtil(root.getTree(user.getPath())); + + NodeUtil folder = userNode.addChild("folder", UserConstants.NT_REP_AUTHORIZABLE_FOLDER); + String path = folder.getTree().getPath(); + try { + // authNode - authFolder -> create User + try { + Principal p = new TestPrincipal("test2"); + Authorizable a = userMgr.createUser(p.getName(), p.getName(), p, path); + root.commit(); + + fail("Users may not be nested."); + } catch (CommitFailedException e) { + // success + } finally { + root.refresh(); + Authorizable a = userMgr.getAuthorizable("test2"); + if (a != null) { + a.remove(); + root.commit(); + } + } + } finally { + root.refresh(); + folder.getTree().remove(); + root.commit(); + } + + NodeUtil someContent = userNode.addChild("mystuff", JcrConstants.NT_UNSTRUCTURED); + path = someContent.getTree().getPath(); + try { + // authNode - anyNode -> create User + try { + Principal p = new TestPrincipal("test3"); + userMgr.createUser(p.getName(), p.getName(), p, path); + root.commit(); + + fail("Users may not be nested."); + } catch (CommitFailedException e) { + // success + } finally { + root.refresh(); + Authorizable a = userMgr.getAuthorizable("test3"); + if (a != null) { + a.remove(); + root.commit(); + } + } + + // authNode - anyNode - authFolder -> create User + folder = someContent.addChild("folder", UserConstants.NT_REP_AUTHORIZABLE_FOLDER); + root.commit(); // this time save node structure + try { + Principal p = new TestPrincipal("test4"); + userMgr.createUser(p.getName(), p.getName(), p, folder.getTree().getPath()); + root.commit(); + + fail("Users may not be nested."); + } catch (CommitFailedException e) { + // success + } finally { + root.refresh(); + Authorizable a = userMgr.getAuthorizable("test4"); + if (a != null) { + a.remove(); + root.commit(); + } + } + } finally { + root.refresh(); + Tree t = root.getTree(path); + if (t != null) { + t.remove(); + root.commit(); + } + } + } + + private class TestPrincipal implements Principal { + + private final String name; + + private TestPrincipal(String name) { + this.name = name; + } + @Override + public String getName() { + return name; + } + } +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserProviderTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserProviderTest.java new file mode 100644 index 00000000000..f65f42a0cd5 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserProviderTest.java @@ -0,0 +1,298 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user; + +import java.util.HashMap; +import java.util.Map; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.plugins.index.IndexHookManager; +import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexHookProvider; +import org.apache.jackrabbit.oak.plugins.nodetype.InitialContent; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.apache.jackrabbit.util.Text; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * UserProviderImplTest... + */ +public class UserProviderTest { + + private Root root; + + private ConfigurationParameters defaultConfig; + private String defaultUserPath; + private String defaultGroupPath; + + private Map customOptions; + private String customUserPath = "/home/users"; + private String customGroupPath = "/home/groups"; + + @Before + public void setUp() throws Exception { + root = new Oak() + .with(new InitialContent()) + .with(new IndexHookManager(new PropertyIndexHookProvider())) + .createRoot(); + + defaultConfig = new ConfigurationParameters(); + defaultUserPath = defaultConfig.getConfigValue(UserConstants.PARAM_USER_PATH, UserConstants.DEFAULT_USER_PATH); + defaultGroupPath = defaultConfig.getConfigValue(UserConstants.PARAM_GROUP_PATH, UserConstants.DEFAULT_GROUP_PATH); + + customOptions = new HashMap(); + customOptions.put(UserConstants.PARAM_GROUP_PATH, customGroupPath); + customOptions.put(UserConstants.PARAM_USER_PATH, customUserPath); + } + + @After + public void tearDown() { + root = null; + } + + private UserProvider createUserProvider() { + return new UserProvider(root, defaultConfig); + } + + private UserProvider createUserProvider(int defaultDepth) { + Map options = new HashMap(customOptions); + options.put(UserConstants.PARAM_DEFAULT_DEPTH, defaultDepth); + return new UserProvider(root, new ConfigurationParameters(options)); + } + + @Test + public void testCreateUser() throws Exception { + UserProvider up = createUserProvider(); + + // create test user + Tree userTree = up.createUser("user1", null); + + assertNotNull(userTree); + assertTrue(Text.isDescendant(defaultUserPath, userTree.getPath())); + int level = defaultConfig.getConfigValue(UserConstants.PARAM_DEFAULT_DEPTH, UserConstants.DEFAULT_DEPTH) + 1; + assertEquals(defaultUserPath, Text.getRelativeParent(userTree.getPath(), level)); + + // make sure all users are created in a structure with default depth + userTree = up.createUser("b", null); + assertEquals(defaultUserPath + "/b/bb/b", userTree.getPath()); + + Map m = new HashMap(); + m.put("bb", "/b/bb/bb"); + m.put("bbb", "/b/bb/bbb"); + m.put("bbbb", "/b/bb/bbbb"); + m.put("bh", "/b/bh/bh"); + m.put("bHbh", "/b/bH/bHbh"); + m.put("b_Hb", "/b/b_/b_Hb"); + m.put("basim", "/b/ba/basim"); + + for (String uid : m.keySet()) { + userTree = up.createUser(uid, null); + assertEquals(defaultUserPath + m.get(uid), userTree.getPath()); + } + } + + @Test + public void testCreateUserWithPath() throws Exception { + UserProvider up = createUserProvider(1); + + // create test user + Tree userTree = up.createUser("nadine", "a/b/c"); + assertNotNull(userTree); + assertTrue(Text.isDescendant(customUserPath, userTree.getPath())); + String userPath = customUserPath + "/a/b/c/nadine"; + assertEquals(userPath, userTree.getPath()); + } + + @Test + public void testCreateGroup() throws RepositoryException { + UserProvider up = createUserProvider(); + + Tree groupTree = up.createGroup("group1", null); + + assertNotNull(groupTree); + assertTrue(Text.isDescendant(defaultGroupPath, groupTree.getPath())); + + int level = defaultConfig.getConfigValue(UserConstants.PARAM_DEFAULT_DEPTH, UserConstants.DEFAULT_DEPTH) + 1; + assertEquals(defaultGroupPath, Text.getRelativeParent(groupTree.getPath(), level)); + } + + @Test + public void testCreateGroupWithPath() throws Exception { + UserProvider up = createUserProvider(4); + + // create test user + Tree group = up.createGroup("authors", "a/b/c"); + assertNotNull(group); + assertTrue(Text.isDescendant(customGroupPath, group.getPath())); + String groupPath = customGroupPath + "/a/b/c/authors"; + assertEquals(groupPath, group.getPath()); + } + + @Test + public void testCreateWithCustomDepth() throws Exception { + UserProvider userProvider = createUserProvider(3); + + Tree userTree = userProvider.createUser("b", null); + assertEquals(customUserPath + "/b/bb/bbb/b", userTree.getPath()); + + Map m = new HashMap(); + m.put("bb", "/b/bb/bbb/bb"); + m.put("bbb", "/b/bb/bbb/bbb"); + m.put("bbbb", "/b/bb/bbb/bbbb"); + m.put("bL", "/b/bL/bLL/bL"); + m.put("bLbh", "/b/bL/bLb/bLbh"); + m.put("b_Lb", "/b/b_/b_L/b_Lb"); + m.put("basiL", "/b/ba/bas/basiL"); + + for (String uid : m.keySet()) { + userTree = userProvider.createUser(uid, null); + assertEquals(customUserPath + m.get(uid), userTree.getPath()); + } + } + + @Test + public void testCreateWithCollision() throws Exception { + UserProvider userProvider = createUserProvider(); + + Tree userTree = userProvider.createUser("AmaLia", null); + + Map colliding = new HashMap(); + colliding.put("AmaLia", null); + colliding.put("AmaLia", "s/ome/path"); + colliding.put("amalia", null); + colliding.put("Amalia", "a/b/c"); + + for (String uid : colliding.keySet()) { + try { + Tree c = userProvider.createUser(uid, colliding.get(uid)); + root.commit(); + fail("userID collision must be detected"); + } catch (CommitFailedException e) { + // success + } + } + + for (String uid : colliding.keySet()) { + try { + Tree c = userProvider.createGroup(uid, colliding.get(uid)); + root.commit(); + fail("userID collision must be detected"); + } catch (CommitFailedException e) { + // success + } + } + } + + @Test + public void testIllegalChars() throws Exception { + UserProvider userProvider = createUserProvider(); + + Map m = new HashMap(); + m.put("z[x]", "/z/" + Text.escapeIllegalJcrChars("z[") + '/' + Text.escapeIllegalJcrChars("z[x]")); + m.put("z*x", "/z/" + Text.escapeIllegalJcrChars("z*") + '/' + Text.escapeIllegalJcrChars("z*x")); + m.put("z/x", "/z/" + Text.escapeIllegalJcrChars("z/") + '/' + Text.escapeIllegalJcrChars("z/x")); + m.put("%\r|", '/' +Text.escapeIllegalJcrChars("%")+ '/' + Text.escapeIllegalJcrChars("%\r") + '/' + Text.escapeIllegalJcrChars("%\r|")); + + for (String uid : m.keySet()) { + Tree user = userProvider.createUser(uid, null); + root.commit(); + + assertEquals(defaultUserPath + m.get(uid), user.getPath()); + assertEquals(uid, userProvider.getAuthorizableId(user)); + + Tree ath = userProvider.getAuthorizable(uid); + assertNotNull("Tree with id " + uid + " must exist.", ath); + } + } + + @Test + public void testGetAuthorizable() throws Exception { + UserProvider up = createUserProvider(); + + String userID = "hannah"; + String groupID = "cLevel"; + + Tree user = up.createUser(userID, null); + Tree group = up.createGroup(groupID, null); + root.commit(); + + Tree a = up.getAuthorizable(userID); + assertNotNull(a); + assertEquals(user.getPath(), a.getPath()); + + a = up.getAuthorizable(groupID); + assertNotNull(a); + assertEquals(group.getPath(), a.getPath()); + } + + @Test + public void testGetAuthorizableByPath() throws Exception { + UserProvider up = createUserProvider(); + + Tree user = up.createUser("shams", null); + Tree a = up.getAuthorizableByPath(user.getPath()); + assertNotNull(a); + assertEquals(user.getPath(), a.getPath()); + + Tree group = up.createGroup("devs", null); + a = up.getAuthorizableByPath(group.getPath()); + assertNotNull(a); + assertEquals(group.getPath(), a.getPath()); + } + + @Test + public void testGetAuthorizableId() throws Exception { + UserProvider up = createUserProvider(); + + String userID = "Amanda"; + Tree user = up.createUser(userID, null); + assertEquals(userID, up.getAuthorizableId(user)); + + String groupID = "visitors"; + Tree group = up.createGroup(groupID, null); + assertEquals(groupID, up.getAuthorizableId(group)); + } + + @Test + public void testRemoveParentTree() throws Exception { + UserProvider up = createUserProvider(); + Tree u1 = up.createUser("b", "b"); + Tree u2 = up.createUser("bb", "bb"); + + Tree folder = root.getTree(Text.getRelativeParent(u1.getPath(), 2)); + folder.remove(); + if (up.getAuthorizable("b") != null) { + fail("Removing the top authorizable folder must remove all users contained."); + u1.remove(); + } + if (up.getAuthorizable("bb") != null) { + fail("Removing the top authorizable folder must remove all users contained."); + u2.remove(); + } + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserValidatorTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserValidatorTest.java new file mode 100644 index 00000000000..0332f8ed9d7 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserValidatorTest.java @@ -0,0 +1,286 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.user; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.security.AbstractSecurityTest; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.apache.jackrabbit.util.Text; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.fail; + +/** + * UserValidatorTest + */ +public class UserValidatorTest extends AbstractSecurityTest { + + private Root root; + private UserManagerImpl userMgr; + private User user; + + @Before + public void before() throws Exception { + super.before(); + + root = admin.getLatestRoot(); + userMgr = new UserManagerImpl(null, root, NamePathMapper.DEFAULT, getSecurityProvider()); + user = userMgr.createUser("test", "pw"); + root.commit(); + } + + @After + public void after() throws Exception { + try { + Authorizable a = userMgr.getAuthorizable("test"); + if (a != null) { + a.remove(); + root.commit(); + } + } finally { + super.after(); + } + } + + @Test + public void removePassword() throws Exception { + try { + Tree userTree = root.getTree(user.getPath()); + userTree.removeProperty(UserConstants.REP_PASSWORD); + root.commit(); + fail("removing password should fail"); + } catch (CommitFailedException e) { + // expected + } finally { + root.refresh(); + } + } + + @Test + public void removePrincipalName() throws Exception { + try { + Tree userTree = root.getTree(user.getPath()); + userTree.removeProperty(UserConstants.REP_PRINCIPAL_NAME); + root.commit(); + fail("removing principal name should fail"); + } catch (CommitFailedException e) { + // expected + } finally { + root.refresh(); + } + } + + @Test + public void removeAuthorizableId() throws Exception { + try { + Tree userTree = root.getTree(user.getPath()); + userTree.removeProperty(UserConstants.REP_AUTHORIZABLE_ID); + root.commit(); + fail("removing authorizable id should fail"); + } catch (CommitFailedException e) { + // expected + } finally { + root.refresh(); + } + } + + @Test + public void createWithoutPrincipalName() throws Exception { + try { + User user = userMgr.createUser("withoutPrincipalName", "pw"); + // TODO: use user.getPath instead (blocked by OAK-343) + Tree tree = root.getTree("/rep:security/rep:authorizables/rep:users/t/te/test"); + tree.removeProperty(UserConstants.REP_PRINCIPAL_NAME); + root.commit(); + + fail("creating user with invalid jcr:uuid should fail"); + } catch (CommitFailedException e) { + // expected + } finally { + root.refresh(); + } + } + + @Test + public void createWithInvalidUUID() throws Exception { + try { + User user = userMgr.createUser("withInvalidUUID", "pw"); + // TODO: use user.getPath instead (blocked by OAK-343) + Tree tree = root.getTree("/rep:security/rep:authorizables/rep:users/t/te/test"); + tree.setProperty(JcrConstants.JCR_UUID, UUID.randomUUID().toString()); + root.commit(); + + fail("creating user with invalid jcr:uuid should fail"); + } catch (CommitFailedException e) { + // expected + } finally { + root.refresh(); + } + } + + @Test + public void changeUUID() throws Exception { + try { + Tree userTree = root.getTree(user.getPath()); + userTree.setProperty(JcrConstants.JCR_UUID, UUID.randomUUID().toString()); + root.commit(); + fail("changing jcr:uuid should fail if it the uuid valid is invalid"); + } catch (CommitFailedException e) { + // expected + } finally { + root.refresh(); + } + } + + @Test + public void changePrincipalName() throws Exception { + try { + Tree userTree = root.getTree(user.getPath()); + userTree.setProperty(UserConstants.REP_PRINCIPAL_NAME, "another"); + root.commit(); + fail("changing the principal name should fail"); + } catch (CommitFailedException e) { + // expected + } finally { + root.refresh(); + } + } + + @Test + public void changeAuthorizableId() throws Exception { + try { + Tree userTree = root.getTree(user.getPath()); + userTree.setProperty(UserConstants.REP_AUTHORIZABLE_ID, "modified"); + root.commit(); + fail("changing the authorizable id should fail"); + } catch (CommitFailedException e) { + // expected + } finally { + root.refresh(); + } + } + + @Test + public void changePasswordToPlainText() throws Exception { + try { + Tree userTree = root.getTree(user.getPath()); + userTree.setProperty(UserConstants.REP_PASSWORD, "plaintext"); + root.commit(); + fail("storing a plaintext password should fail"); + } catch (CommitFailedException e) { + // expected + } finally { + root.refresh(); + } + } + + @Test + public void testRemoveAdminUser() throws Exception { + try { + String adminId = userMgr.getConfig().getConfigValue(UserConstants.PARAM_ADMIN_ID, UserConstants.DEFAULT_ADMIN_ID); + Authorizable admin = userMgr.getAuthorizable(adminId); + if (admin == null) { + admin = userMgr.createUser(adminId, adminId); + root.commit(); + } + + root.getTree(admin.getPath()).remove(); + root.commit(); + fail("Admin user cannot be removed"); + } catch (CommitFailedException e) { + // success + } finally { + root.refresh(); + } + } + + @Test + public void testDisableAdminUser() throws Exception { + try { + String adminId = userMgr.getConfig().getConfigValue(UserConstants.PARAM_ADMIN_ID, UserConstants.DEFAULT_ADMIN_ID); + Authorizable admin = userMgr.getAuthorizable(adminId); + if (admin == null) { + admin = userMgr.createUser(adminId, adminId); + root.commit(); + } + + root.getTree(admin.getPath()).setProperty(UserConstants.REP_DISABLED, "disabled"); + root.commit(); + fail("Admin user cannot be disabled"); + } catch (CommitFailedException e) { + // success + } finally { + root.refresh(); + } + } + + @Test + public void testEnforceHierarchy() throws RepositoryException, CommitFailedException { + List invalid = new ArrayList(); + invalid.add("/"); + invalid.add("/jcr:system"); + String groupPath = userMgr.getConfig().getConfigValue(UserConstants.PARAM_GROUP_PATH, UserConstants.DEFAULT_GROUP_PATH); + invalid.add(groupPath); + String userPath = userMgr.getConfig().getConfigValue(UserConstants.PARAM_USER_PATH, UserConstants.DEFAULT_USER_PATH); + invalid.add(Text.getRelativeParent(userPath, 1)); + invalid.add(user.getPath()); + invalid.add(user.getPath() + "/folder"); + + for (String path : invalid) { + try { + Tree parent = root.getTree(path); + if (parent == null) { + String[] segments = Text.explode(path, '/', false); + parent = root.getTree("/"); + for (String segment : segments) { + Tree next = parent.getChild(segment); + if (next == null) { + next = parent.addChild(segment); + next.setProperty(JcrConstants.JCR_PRIMARYTYPE, UserConstants.NT_REP_AUTHORIZABLE_FOLDER); + parent = next; + } + } + } + Tree userTree = parent.addChild("testUser"); + userTree.setProperty(JcrConstants.JCR_PRIMARYTYPE, UserConstants.NT_REP_USER); + userTree.setProperty(JcrConstants.JCR_UUID, UserProvider.getContentID("testUser")); + userTree.setProperty(UserConstants.REP_PRINCIPAL_NAME, "testUser"); + root.commit(); + fail("Invalid hierarchy should be detected"); + + } catch (CommitFailedException e) { + // success + } finally { + root.refresh(); + } + } + } + +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/commit/SubtreeValidatorTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/commit/SubtreeValidatorTest.java new file mode 100644 index 00000000000..057478239df --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/commit/SubtreeValidatorTest.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.spi.commit; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; +import static org.apache.jackrabbit.oak.plugins.memory.MemoryNodeState.EMPTY_NODE; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.junit.Test; + +public class SubtreeValidatorTest { + + + @Test + public void testSubtreeValidator() throws CommitFailedException { + Validator delegate = new FailingValidator(); + Validator validator = new SubtreeValidator(delegate, "one", "two"); + + assertNull(validator.childNodeAdded("zero", EMPTY_NODE)); + assertNull(validator.childNodeChanged("two", EMPTY_NODE, EMPTY_NODE)); + assertNull(validator.childNodeDeleted("foo", EMPTY_NODE)); + + assertNotNull(validator.childNodeAdded("one", EMPTY_NODE)); + assertNotNull(validator.childNodeChanged("one", EMPTY_NODE, EMPTY_NODE)); + assertNotNull(validator.childNodeDeleted("one", EMPTY_NODE)); + + // Descend to the subtree + validator = validator.childNodeChanged("one", EMPTY_NODE, EMPTY_NODE); + assertNull(validator.childNodeAdded("zero", EMPTY_NODE)); + assertNull(validator.childNodeChanged("one", EMPTY_NODE, EMPTY_NODE)); + assertNull(validator.childNodeDeleted("foo", EMPTY_NODE)); + + assertEquals(delegate, validator.childNodeAdded("two", EMPTY_NODE)); + assertEquals(delegate, validator.childNodeChanged("two", EMPTY_NODE, EMPTY_NODE)); + assertEquals(delegate, validator.childNodeDeleted("two", EMPTY_NODE)); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/AbstractLoginModuleTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/AbstractLoginModuleTest.java new file mode 100644 index 00000000000..ba06e49a251 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/AbstractLoginModuleTest.java @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.authentication; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.jcr.Credentials; +import javax.jcr.SimpleCredentials; +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.oak.spi.security.authentication.callback.CredentialsCallback; +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; + +/** + * AbstractLoginModuleTest... + */ +public class AbstractLoginModuleTest { + + private AbstractLoginModule initLoginModule(Class supportedCredentials, Map sharedState) { + AbstractLoginModule lm = new TestLoginModule(supportedCredentials); + lm.initialize(new Subject(), null, sharedState, null); + return lm; + } + + + private AbstractLoginModule initLoginModule(Class supportedCredentials, CallbackHandler cbh) { + AbstractLoginModule lm = new TestLoginModule(supportedCredentials); + lm.initialize(new Subject(), cbh, Collections.emptyMap(), null); + return lm; + } + + @Test + public void testGetSharedLoginName() { + Map sharedState = new HashMap(); + + sharedState.put(AbstractLoginModule.SHARED_KEY_LOGIN_NAME, "test"); + AbstractLoginModule lm = initLoginModule(TestCredentials.class, sharedState); + assertEquals("test", lm.getSharedLoginName()); + + sharedState.clear(); + lm = initLoginModule(TestCredentials.class, sharedState); + assertNull(lm.getSharedLoginName()); + } + + @Test + public void testGetSharedCredentials() { + Map sharedState = new HashMap(); + + sharedState.put(AbstractLoginModule.SHARED_KEY_CREDENTIALS, new TestCredentials()); + AbstractLoginModule lm = initLoginModule(TestCredentials.class, sharedState); + assertTrue(lm.getSharedCredentials() instanceof TestCredentials); + + sharedState.put(AbstractLoginModule.SHARED_KEY_CREDENTIALS, new SimpleCredentials("test", "test".toCharArray())); + lm = initLoginModule(TestCredentials.class, sharedState); + assertTrue(lm.getSharedCredentials() instanceof SimpleCredentials); + + lm = initLoginModule(SimpleCredentials.class, sharedState); + assertTrue(lm.getSharedCredentials() instanceof SimpleCredentials); + + sharedState.put(AbstractLoginModule.SHARED_KEY_CREDENTIALS, "no credentials object"); + lm = initLoginModule(TestCredentials.class, sharedState); + assertNull(lm.getSharedCredentials()); + + sharedState.clear(); + lm = initLoginModule(TestCredentials.class, sharedState); + assertNull(lm.getSharedCredentials()); + } + + @Test + public void testGetCredentialsFromSharedState() { + Map sharedState = new HashMap(); + + sharedState.put(AbstractLoginModule.SHARED_KEY_CREDENTIALS, new TestCredentials()); + AbstractLoginModule lm = initLoginModule(TestCredentials.class, sharedState); + assertTrue(lm.getCredentials() instanceof TestCredentials); + + SimpleCredentials sc = new SimpleCredentials("test", "test".toCharArray()); + sharedState.put(AbstractLoginModule.SHARED_KEY_CREDENTIALS, sc); + lm = initLoginModule(TestCredentials.class, sharedState); + assertNull(lm.getCredentials()); + + sharedState.put(AbstractLoginModule.SHARED_KEY_CREDENTIALS, sc); + lm = initLoginModule(SimpleCredentials.class, sharedState); + assertTrue(lm.getCredentials() instanceof SimpleCredentials); + + sharedState.clear(); + lm = initLoginModule(TestCredentials.class, sharedState); + assertNull(lm.getCredentials()); + } + + public void testGetCredentialsFromCallbackHandler() { + CallbackHandler cbh = new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) { + for (Callback cb : callbacks) { + if (cb instanceof CredentialsCallback) { + ((CredentialsCallback) cb).setCredentials(new TestCredentials()); + } + } + } + }; + + AbstractLoginModule lm = initLoginModule(TestCredentials.class, cbh); + assertTrue(lm.getCredentials() instanceof TestCredentials); + + lm = initLoginModule(SimpleCredentials.class, cbh); + assertNull(lm.getCredentials()); + } + + //-------------------------------------------------------------------------- + + private final class TestCredentials implements Credentials {} + + private final class TestLoginModule extends AbstractLoginModule { + + private Class supportedCredentialsClass; + + private TestLoginModule(Class supportedCredentialsClass) { + this.supportedCredentialsClass = supportedCredentialsClass; + } + + @Nonnull + @Override + protected Set getSupportedCredentials() { + return Collections.singleton(supportedCredentialsClass); + } + + @Override + public boolean login() throws LoginException { + return true; + } + + @Override + public boolean commit() throws LoginException { + return true; + } + } +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/GuestLoginModuleTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/GuestLoginModuleTest.java new file mode 100644 index 00000000000..cdf90fe34bf --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/GuestLoginModuleTest.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.authentication; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.jcr.Credentials; +import javax.jcr.GuestCredentials; +import javax.jcr.SimpleCredentials; +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +import org.apache.jackrabbit.oak.spi.security.authentication.callback.CredentialsCallback; +import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * GuestLoginModuleTest... + */ +public class GuestLoginModuleTest { + + private LoginModule guestLoginModule = new GuestLoginModule(); + + @Test + public void testNullLogin() throws LoginException { + Subject subject = new Subject(); + CallbackHandler cbh = new TestCallbackHandler(null); + Map sharedState = new HashMap(); + guestLoginModule.initialize(subject, cbh, sharedState, Collections.emptyMap()); + + assertTrue(guestLoginModule.login()); + Object sharedCreds = sharedState.get(AbstractLoginModule.SHARED_KEY_CREDENTIALS); + assertNotNull(sharedCreds); + assertTrue(sharedCreds instanceof GuestCredentials); + + assertTrue(guestLoginModule.commit()); + assertFalse(subject.getPrincipals(EveryonePrincipal.class).isEmpty()); + assertFalse(subject.getPublicCredentials(GuestCredentials.class).isEmpty()); + } + + @Test + public void testGuestCredentials() throws LoginException { + Subject subject = new Subject(); + CallbackHandler cbh = new TestCallbackHandler(new GuestCredentials()); + Map sharedState = new HashMap(); + guestLoginModule.initialize(subject, cbh, sharedState, Collections.emptyMap()); + + assertFalse(guestLoginModule.login()); + assertFalse(sharedState.containsKey(AbstractLoginModule.SHARED_KEY_CREDENTIALS)); + + assertFalse(guestLoginModule.commit()); + assertTrue(subject.getPrincipals().isEmpty()); + assertTrue(subject.getPublicCredentials().isEmpty()); + } + + @Test + public void testSimpleCredentials() throws LoginException { + Subject subject = new Subject(); + CallbackHandler cbh = new TestCallbackHandler(new SimpleCredentials("test", new char[0])); + Map sharedState = new HashMap(); + guestLoginModule.initialize(subject, cbh, sharedState, Collections.emptyMap()); + + assertFalse(guestLoginModule.login()); + assertFalse(sharedState.containsKey(AbstractLoginModule.SHARED_KEY_CREDENTIALS)); + + assertFalse(guestLoginModule.commit()); + assertTrue(subject.getPrincipals().isEmpty()); + assertTrue(subject.getPublicCredentials().isEmpty()); + } + + //-------------------------------------------------------------------------- + + private class TestCallbackHandler implements CallbackHandler { + + private final Credentials creds; + + private TestCallbackHandler(Credentials creds) { + this.creds = creds; + } + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof CredentialsCallback) { + ((CredentialsCallback) callback).setCredentials(creds); + } else { + throw new UnsupportedCallbackException(callback); + } + } + } + } +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/principal/EveryonePrincipalTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/principal/EveryonePrincipalTest.java new file mode 100644 index 00000000000..5ec929d7358 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/principal/EveryonePrincipalTest.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.principal; + +import java.security.Principal; +import java.util.Enumeration; + +import org.apache.jackrabbit.api.security.principal.JackrabbitPrincipal; +import org.apache.jackrabbit.oak.security.AbstractSecurityTest; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class EveryonePrincipalTest extends AbstractSecurityTest { + + private final Principal everyone = EveryonePrincipal.getInstance(); + + @Test + public void testGetName() { + assertEquals(EveryonePrincipal.NAME, everyone.getName()); + } + + @Test + public void testEquals() { + assertEquals(everyone, EveryonePrincipal.getInstance()); + } + + @Test + public void testSame() { + assertSame(everyone, EveryonePrincipal.getInstance()); + } + + @Test + public void testHashCode() { + assertTrue(everyone.hashCode() == EveryonePrincipal.getInstance().hashCode()); + } + + @Test + public void testNotEqualsOtherPrincipalWithSameName() { + Principal someotherEveryone = new Principal() { + public String getName() { + return EveryonePrincipal.NAME; + } + }; + assertFalse(everyone.equals(someotherEveryone)); + } + + @Test + public void testEqualsOtherJackrabbitPrincipal() { + Principal someotherEveryone = new OtherEveryone(); + + assertEquals(everyone, someotherEveryone); + } + + @Test + public void testEqualsOtherJackrabbitGroup() { + Principal someotherEveryone = new OtherEveryoneGroup(); + + assertEquals(everyone, someotherEveryone); + } + + //-------------------------------------------------------------------------- + + private class OtherEveryone implements JackrabbitPrincipal { + public String getName() { + return EveryonePrincipal.NAME; + } + @Override + public boolean equals(Object o) { + if (o instanceof JackrabbitPrincipal) { + return getName().equals(((JackrabbitPrincipal) o).getName()); + } + return false; + } + @Override + public int hashCode() { + return getName().hashCode(); + } + } + + private class OtherEveryoneGroup extends OtherEveryone implements java.security.acl.Group { + + @Override + public boolean addMember(Principal principal) { + // TODO + return false; + } + + @Override + public boolean removeMember(Principal principal) { + // TODO + return false; + } + + @Override + public boolean isMember(Principal principal) { + // TODO + return false; + } + + @Override + public Enumeration members() { + // TODO + return null; + } + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/user/action/PasswordValidationActionTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/user/action/PasswordValidationActionTest.java new file mode 100644 index 00000000000..3f059a6a4c2 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/user/action/PasswordValidationActionTest.java @@ -0,0 +1,200 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.user.action; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.annotation.Nonnull; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.security.AbstractSecurityTest; +import org.apache.jackrabbit.oak.security.SecurityProviderImpl; +import org.apache.jackrabbit.oak.security.user.UserConfigurationImpl; +import org.apache.jackrabbit.oak.security.user.UserManagerImpl; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.apache.jackrabbit.oak.spi.security.user.util.PasswordUtility; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class PasswordValidationActionTest extends AbstractSecurityTest { + + private PasswordValidationAction pwAction = new PasswordValidationAction(); + private TestAction testAction = new TestAction(); + + private Root root; + private UserManager userManager; + private User user; + + private User testUser; + + @Before + public void before() throws Exception { + super.before(); + + root = admin.getLatestRoot(); + + userManager = new UserManagerImpl(null, root, NamePathMapper.DEFAULT, getSecurityProvider()); + user = (User) userManager.getAuthorizable(admin.getAuthInfo().getUserID()); + + pwAction.setConstraint("^.*(?=.{8,})(?=.*[a-z])(?=.*[A-Z]).*"); + + } + + @After + public void after() throws Exception { + if (testUser != null) { + testUser.remove(); + root.commit(); + } + root = null; + super.after(); + } + + @Override + protected SecurityProvider getSecurityProvider() { + if (securityProvider == null) { + securityProvider = new TestSecurityProvider(); + } + return securityProvider; + } + + @Test + public void testActionIsCalled() throws Exception { + testUser = userManager.createUser("testUser", "testUser12345"); + root.commit(); + assertEquals(1, testAction.onCreateCalled); + + testUser.changePassword("pW12345678"); + assertEquals(1, testAction.onPasswordChangeCalled); + + testUser.changePassword("pW1234567890", "pW12345678"); + assertEquals(2, testAction.onPasswordChangeCalled); + } + + @Test + public void testPasswordValidationAction() throws Exception { + List invalid = new ArrayList(); + invalid.add("pw1"); + invalid.add("only6C"); + invalid.add("12345678"); + invalid.add("WITHOUTLOWERCASE"); + invalid.add("withoutuppercase"); + + for (String pw : invalid) { + try { + pwAction.onPasswordChange(user, pw, root, NamePathMapper.DEFAULT); + fail("should throw constraint violation"); + } catch (ConstraintViolationException e) { + // success + } + } + + List valid = new ArrayList(); + valid.add("abCDefGH"); + valid.add("Abbbbbbbbbbbb"); + valid.add("cDDDDDDDDDDDDDDDDD"); + valid.add("gH%%%%%%%%%%%%%%%%^^"); + valid.add("&)(*&^%23qW"); + + for (String pw : valid) { + pwAction.onPasswordChange(user, pw, root, NamePathMapper.DEFAULT); + } + } + + @Test + public void testPasswordValidationActionOnCreate() throws Exception { + String hashed = PasswordUtility.buildPasswordHash("DWkej32H"); + testUser = userManager.createUser("testuser", hashed); + root.commit(); + + String pwValue = root.getTree(testUser.getPath()).getProperty(UserConstants.REP_PASSWORD).getValue(Type.STRING); + assertFalse(PasswordUtility.isPlainTextPassword(pwValue)); + assertTrue(PasswordUtility.isSame(pwValue, hashed)); + } + + @Test + public void testPasswordValidationActionOnChange() throws Exception { + testUser = userManager.createUser("testuser", "testPw123456"); + root.commit(); + try { + pwAction.setConstraint("abc"); + + String hashed = PasswordUtility.buildPasswordHash("abc"); + testUser.changePassword(hashed); + + fail("Password change must always enforce password validation."); + + } catch (ConstraintViolationException e) { + // success + } + } + + //-------------------------------------------------------------------------- + private class TestAction extends AbstractAuthorizableAction { + + private int onCreateCalled = 0; + private int onPasswordChangeCalled = 0; + + @Override + public void onCreate(User user, String password, Root root, NamePathMapper namePathMapper) throws RepositoryException { + onCreateCalled++; + } + + @Override + public void onPasswordChange(User user, String newPassword, Root root, NamePathMapper namePathMapper) throws RepositoryException { + onPasswordChangeCalled++; + } + } + + private class TestSecurityProvider extends SecurityProviderImpl { + + private final AuthorizableAction[] actions; + + private TestSecurityProvider() { + this.actions = new AuthorizableAction[] {pwAction, testAction}; + } + + @Nonnull + @Override + public UserConfiguration getUserConfiguration() { + return new UserConfigurationImpl(ConfigurationParameters.EMPTY, this) { + + @Nonnull + @Override + public List getAuthorizableActions() { + return Arrays.asList(actions); + } + }; + } + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/user/util/PasswordUtilityTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/user/util/PasswordUtilityTest.java new file mode 100644 index 00000000000..74e25ced402 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/user/util/PasswordUtilityTest.java @@ -0,0 +1,150 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.spi.security.user.util; + +import org.apache.jackrabbit.oak.spi.security.user.util.PasswordUtility; +import org.junit.Test; + +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class PasswordUtilityTest { + + private static List PLAIN_PWDS = new ArrayList(); + static { + PLAIN_PWDS.add("pw"); + PLAIN_PWDS.add("PassWord123"); + PLAIN_PWDS.add("_"); + PLAIN_PWDS.add("{invalidAlgo}"); + PLAIN_PWDS.add("{invalidAlgo}Password"); + PLAIN_PWDS.add("{SHA-256}"); + PLAIN_PWDS.add("pw{SHA-256}"); + PLAIN_PWDS.add("p{SHA-256}w"); + PLAIN_PWDS.add(""); + } + + private static Map HASHED_PWDS = new HashMap(); + static { + for (String pw : PLAIN_PWDS) { + try { + HASHED_PWDS.put(pw, PasswordUtility.buildPasswordHash(pw)); + } catch (Exception e) { + // should not get here + } + } + } + + @Test + public void testBuildPasswordHash() throws Exception { + for (String pw : PLAIN_PWDS) { + String pwHash = PasswordUtility.buildPasswordHash(pw); + assertFalse(pw.equals(pwHash)); + } + + List l = new ArrayList(); + l.add(new Integer[] {0, 1000}); + l.add(new Integer[] {1, 10}); + l.add(new Integer[] {8, 50}); + l.add(new Integer[] {10, 5}); + l.add(new Integer[] {-1, -1}); + for (Integer[] params : l) { + for (String pw : PLAIN_PWDS) { + int saltsize = params[0]; + int iterations = params[1]; + + String pwHash = PasswordUtility.buildPasswordHash(pw, PasswordUtility.DEFAULT_ALGORITHM, saltsize, iterations); + assertFalse(pw.equals(pwHash)); + } + } + } + + @Test + public void testBuildPasswordHashInvalidAlgorithm() throws Exception { + List invalidAlgorithms = new ArrayList(); + invalidAlgorithms.add(""); + invalidAlgorithms.add("+"); + invalidAlgorithms.add("invalid"); + + for (String invalid : invalidAlgorithms) { + try { + String pwHash = PasswordUtility.buildPasswordHash("pw", invalid, PasswordUtility.DEFAULT_SALT_SIZE, PasswordUtility.DEFAULT_ITERATIONS); + fail("Invalid algorithm " + invalid); + } catch (NoSuchAlgorithmException e) { + // success + } + } + + } + + @Test + public void testIsPlainTextPassword() throws Exception { + for (String pw : PLAIN_PWDS) { + assertTrue(pw + " should be plain text.", PasswordUtility.isPlainTextPassword(pw)); + } + } + + @Test + public void testIsPlainTextForNull() throws Exception { + assertTrue(PasswordUtility.isPlainTextPassword(null)); + } + + @Test + public void testIsPlainTextForPwHash() throws Exception { + for (String pwHash : HASHED_PWDS.values()) { + assertFalse(pwHash + " should not be plain text.", PasswordUtility.isPlainTextPassword(pwHash)); + } + } + + @Test + public void testIsSame() throws Exception { + for (String pw : HASHED_PWDS.keySet()) { + String pwHash = HASHED_PWDS.get(pw); + assertTrue("Not the same " + pw + ", " + pwHash, PasswordUtility.isSame(pwHash, pw)); + } + + String pw = "password"; + String pwHash = PasswordUtility.buildPasswordHash(pw, "SHA-1", 4, 50); + assertTrue("Not the same '" + pw + "', " + pwHash, PasswordUtility.isSame(pwHash, pw)); + + pwHash = PasswordUtility.buildPasswordHash(pw, "md5", 0, 5); + assertTrue("Not the same '" + pw + "', " + pwHash, PasswordUtility.isSame(pwHash, pw)); + + pwHash = PasswordUtility.buildPasswordHash(pw, "md5", -1, -1); + assertTrue("Not the same '" + pw + "', " + pwHash, PasswordUtility.isSame(pwHash, pw)); + } + + @Test + public void testIsNotSame() throws Exception { + String previous = null; + for (String pw : HASHED_PWDS.keySet()) { + String pwHash = HASHED_PWDS.get(pw); + assertFalse(pw, PasswordUtility.isSame(pw, pw)); + assertFalse(pwHash, PasswordUtility.isSame(pwHash, pwHash)); + if (previous != null) { + assertFalse(previous, PasswordUtility.isSame(pwHash, previous)); + } + previous = pw; + } + } +} \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/ArrayUtilsTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/util/ArrayUtilsTest.java similarity index 91% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/util/ArrayUtilsTest.java rename to oak-core/src/test/java/org/apache/jackrabbit/oak/util/ArrayUtilsTest.java index 4b06c9c7209..f11071244d1 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/ArrayUtilsTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/util/ArrayUtilsTest.java @@ -14,11 +14,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.util; +package org.apache.jackrabbit.oak.util; + +import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import org.junit.Test; /** * Tests the ArrayUtils class @@ -49,8 +50,8 @@ public void insertLong() { @Test public void insertObject() { - Long[] x = {Long.valueOf(10), Long.valueOf(20)}; - Long[] y = ArrayUtils.arrayInsert(x, 1, Long.valueOf(15)); + Long[] x = {10L, 20L}; + Long[] y = ArrayUtils.arrayInsert(x, 1, 15L); assertFalse(x == y); assertEquals(3, y.length); assertEquals(Long.valueOf(10), y[0]); @@ -93,7 +94,7 @@ public void removeLong() { @Test public void removeObject() { - Long[] x = {Long.valueOf(10), Long.valueOf(20)}; + Long[] x = {10L, 20L}; Long[] y = ArrayUtils.arrayRemove(x, 1); assertFalse(x == y); assertEquals(1, y.length); @@ -115,8 +116,8 @@ public void removeString() { @Test public void replaceObject() { - Long[] x = {Long.valueOf(10), Long.valueOf(20)}; - Long[] y = ArrayUtils.arrayReplace(x, 1, Long.valueOf(11)); + Long[] x = {10L, 20L}; + Long[] y = ArrayUtils.arrayReplace(x, 1, 11L); assertFalse(x == y); assertEquals(2, y.length); assertEquals(Long.valueOf(10), y[0]); diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/util/JsopUtilTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/util/JsopUtilTest.java new file mode 100644 index 00000000000..c6e8fb5dd6e --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/util/JsopUtilTest.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.util; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; +import static org.apache.jackrabbit.oak.api.Type.STRING; + +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.query.JsopUtil; +import org.junit.Test; + +public class JsopUtilTest { + + @Test + public void test() throws Exception { + Root root = new Oak().createRoot(); + + Tree t = root.getTree("/"); + assertFalse(t.hasChild("test")); + + String add = "/ + \"test\": { \"a\": { \"id\": \"123\" }, \"b\": {} }"; + JsopUtil.apply(root, add); + root.commit(); + + t = root.getTree("/"); + assertTrue(t.hasChild("test")); + + t = t.getChild("test"); + assertEquals(2, t.getChildrenCount()); + assertTrue(t.hasChild("a")); + assertTrue(t.hasChild("b")); + + assertEquals(0, t.getChild("b").getChildrenCount()); + + t = t.getChild("a"); + assertEquals(0, t.getChildrenCount()); + assertTrue(t.hasProperty("id")); + assertEquals("123", t.getProperty("id").getValue(STRING)); + + String rm = "/ - \"test\""; + JsopUtil.apply(root, rm); + root.commit(); + + t = root.getTree("/"); + assertFalse(t.hasChild("test")); + } + +} diff --git a/oak-core/src/test/resources/META-INF/services/org.apache.jackrabbit.mk.test.MicroKernelFixture b/oak-core/src/test/resources/META-INF/services/org.apache.jackrabbit.mk.test.MicroKernelFixture new file mode 100644 index 00000000000..62eb7af05e4 --- /dev/null +++ b/oak-core/src/test/resources/META-INF/services/org.apache.jackrabbit.mk.test.MicroKernelFixture @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#org.apache.jackrabbit.mk.simple.SimpleKernelImplFixture diff --git a/oak-core/src/test/resources/logback-test.xml b/oak-core/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..e92401c93ea --- /dev/null +++ b/oak-core/src/test/resources/logback-test.xml @@ -0,0 +1,39 @@ + + + + + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + target/unit-tests.log + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + + + + + diff --git a/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql1.txt b/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql1.txt new file mode 100644 index 00000000000..a1d1fe526d0 --- /dev/null +++ b/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql1.txt @@ -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. +# +# Syntax: +# * lines starting with "#" are remarks. +# * lines starting with "select" are queries, followed by expected results and an empty line +# * lines starting with "explain" are followed by expected query plan and an empty line +# * lines starting with "sql1" are run using the sql1 language +# * lines starting with "xpath2sql" are just converted from xpath to sql2 +# * all other lines are are committed into the microkernel (line by line) +# * new tests are typically be added on top, after the syntax docs +# * use ascii character only + +# sql-1 query (nt:unstructured needs to be escaped in sql-2) + +sql1 select prop1 from nt:unstructured where prop1 is not null order by prop1 asc + +sql1 select * from nt:base where jcr:path like '/testroot/%' and birth > timestamp '1976-01-01T00:00:00.000+01:00' + +sql1 select * from nt:base where jcr:path like '/testroot/%' and value like 'foo\_bar' escape '\' + +sql1 select * from nt:unstructured where "jcr:path" = '/testroot/foo' and contains(., 'fox') + +sql1 select * from nt:unstructured where "jcr:path" like '/testroot/%' and contains(., 'fox test') + +# not supported currently +# sql1 select [jcr:path], [jcr:score], * from [nt:base] where (0 is not null) and isdescendantnode('/testroot') + diff --git a/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2.txt b/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2.txt new file mode 100644 index 00000000000..11a2c333571 --- /dev/null +++ b/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2.txt @@ -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. +# +# Syntax: +# * lines starting with "#" are remarks. +# * lines starting with "select" are queries, followed by expected results and an empty line +# * lines starting with "explain" are followed by expected query plan and an empty line +# * lines starting with "sql1" are run using the sql1 language +# * lines starting with "xpath2sql" are just converted from xpath to sql2 +# * all other lines are are committed into the microkernel (line by line) +# * new tests are typically be added on top, after the syntax docs +# * use ascii character only + +# test multi-valued properties + +commit / + "test": { "a": { "name": ["Hello", "World" ] }, "b": { "name" : "Hello" }} + +select * from [nt:base] where name = 'Hello' +/test/a +/test/b + +select * from [nt:base] where name = 'World' +/test/a + +select * from [nt:base] where isdescendantnode('/test') and name = 'World' +/test/a + +commit / - "test" + +# expected error on two selectors with the same name + +select * from [nt:base] as p inner join [nt:base] as p on ischildnode(p, p) where p.[jcr:path] = '/' +java.text.ParseException: select * from [nt:base] as p inner join [nt:base] as p on ischildnode(p, p) where p.[jcr:path] = '/': Two selectors with the same name: p + +# combining 'not' and 'and' + +commit / + "test": { "a": { "id": "10" }, "b": { "id" : "20" }} + +select * from [nt:base] where id is not null and not id = '100' and id <> '20' +/test/a + +select * from [nt:base] where id < '1000' +/test/a + +select * from [nt:base] where id is not null and not (id = '100' and id <> '20') +/test/a +/test/b + +select * from [nt:base] where id = '10' +/test/a + +select [jcr:path], * from [nt:base] where id = '10' +/test/a, null + +select * from [nt:base] where id > '10' +/test/b + +commit / - "test" + +# fulltext search + +commit / + "test": { "name": "hello world" } + +select * from [nt:base] where contains(name, 'hello') +/test + +select * from [nt:base] where contains(*, 'hello') +/test + +commit / - "test" + +# other tests + +select [jcr:path] from [nt:base] as a where issamenode(a, '/') +/ + +commit / + "test": { "My Documents": { "x" : {}}} + +select [jcr:path] from [nt:base] where name() = 'My_x0020_Documents' +/test/My Documents + +commit / - "test" + +commit / + "test": { "jcr:resource": {}, "resource": { "x" : {}}} + +select * from [nt:base] where id = -1 + +select * from [nt:base] as b where isdescendantnode(b, '/test') +/test/jcr:resource +/test/resource +/test/resource/x + +select * from [nt:base] as b where ischildnode(b, '/test') +/test/jcr:resource +/test/resource + +select * from [nt:base] as b where issamenode(b, '/test') +/test + +select * from [nt:base] where name() = 'resource' +/test/resource + +select * from [nt:base] as b where localname(b) = 'resource' +/jcr:system/jcr:nodeTypes/nt:resource +/test/jcr:resource +/test/resource + +select * from [nt:base] as x where isdescendantnode(x, '/') and not isdescendantnode(x, '/jcr:system') +/jcr:system +/oak:index +/oak:index/authorizableId +/oak:index/members +/oak:index/primaryType +/oak:index/principalName +/oak:index/test-index +/oak:index/uuid +/rep:security +/rep:security/rep:authorizables +/rep:security/rep:authorizables/rep:users +/rep:security/rep:authorizables/rep:users/a +/rep:security/rep:authorizables/rep:users/a/ad +/rep:security/rep:authorizables/rep:users/a/ad/admin +/rep:security/rep:authorizables/rep:users/a/an +/rep:security/rep:authorizables/rep:users/a/an/anonymous +/test +/test/jcr:resource +/test/resource +/test/resource/x + +commit / - "test" + +commit / + "parents": { "p0": {"id": "0"}, "p1": {"id": "1"}, "p2": {"id": "2"}} +commit / + "children": { "c1": {"p": "1"}, "c2": {"p": "1"}, "c3": {"p": "2"}, "c4": {"p": "3"}} + +# relative property +select * from [nt:base] where [c1/p] = '1' +/children + +select * from [nt:base] as p where p.[jcr:path] = '/parents' +/parents + +select * from [nt:base] as [p] where [p].[jcr:path] = '/parents' +/parents + +select * from [nt:base] as p inner join [nt:base] as p2 on ischildnode(p2, p) where p.[jcr:path] = '/' +/, /children +/, /jcr:system +/, /oak:index +/, /parents +/, /rep:security + +select * from [nt:base] as p inner join [nt:base] as p2 on isdescendantnode(p2, p) where p.[jcr:path] = '/parents' +/parents, /parents/p0 +/parents, /parents/p1 +/parents, /parents/p2 + +select * from [nt:base] as p inner join [nt:base] as p2 on issamenode(p2, p) where p.[jcr:path] = '/parents' +/parents, /parents + +select id from [nt:base] where id is not null +0 +1 +2 + +select id from [nt:base] where id is not null order by id desc +2 +1 +0 + +select * from [nt:base] as c right outer join [nt:base] as p on p.id = c.p where p.id is not null and not isdescendantnode(p, '/jcr:system') +/children/c1, /parents/p1 +/children/c2, /parents/p1 +/children/c3, /parents/p2 +null, /parents/p0 + +select * from [nt:base] as p left outer join [nt:base] as c on p.id = c.p where p.id is not null +/parents/p0, null +/parents/p1, /children/c1 +/parents/p1, /children/c2 +/parents/p2, /children/c3 + +select * from [nt:base] as p left outer join [nt:base] as c on p.id = c.p where p.id is not null and c.p is null +/parents/p0, null + +select * from [nt:base] as p left outer join [nt:base] as c on p.id = c.p where p.id is not null and c.p is not null +/parents/p1, /children/c1 +/parents/p1, /children/c2 +/parents/p2, /children/c3 + +select * from [nt:base] as p inner join [nt:base] as c on p.id = c.p +/parents/p1, /children/c1 +/parents/p1, /children/c2 +/parents/p2, /children/c3 + +commit / - "parents" +commit / - "children" + +commit / + "test": { "hello": { "x": "1" }, "world": { "x": "2" } } +commit / + "test2": { "id":"1", "x": "2" } + +select * from [nt:base] where not isdescendantnode('/jcr:system') +/ +/jcr:system +/oak:index +/oak:index/authorizableId +/oak:index/members +/oak:index/primaryType +/oak:index/principalName +/oak:index/test-index +/oak:index/uuid +/rep:security +/rep:security/rep:authorizables +/rep:security/rep:authorizables/rep:users +/rep:security/rep:authorizables/rep:users/a +/rep:security/rep:authorizables/rep:users/a/ad +/rep:security/rep:authorizables/rep:users/a/ad/admin +/rep:security/rep:authorizables/rep:users/a/an +/rep:security/rep:authorizables/rep:users/a/an/anonymous +/test +/test/hello +/test/world +/test2 + +select * from [nt:base] where id = '1' +/test2 + +select * from [nt:base] where id = '1' and x = '2' +/test2 + +select * from [nt:base] where id = '1' or x = '2' +/test/world +/test2 + +select * from [nt:base] where not (id = '1' or x = '2') and not isdescendantnode('/jcr:system') +/ +/jcr:system +/oak:index +/oak:index/authorizableId +/oak:index/members +/oak:index/primaryType +/oak:index/principalName +/oak:index/test-index +/oak:index/uuid +/rep:security +/rep:security/rep:authorizables +/rep:security/rep:authorizables/rep:users +/rep:security/rep:authorizables/rep:users/a +/rep:security/rep:authorizables/rep:users/a/ad +/rep:security/rep:authorizables/rep:users/a/ad/admin +/rep:security/rep:authorizables/rep:users/a/an +/rep:security/rep:authorizables/rep:users/a/an/anonymous +/test +/test/hello + +select * from [nt:base] where x is null and not isdescendantnode('/jcr:system') +/ +/jcr:system +/oak:index +/oak:index/authorizableId +/oak:index/members +/oak:index/primaryType +/oak:index/principalName +/oak:index/test-index +/oak:index/uuid +/rep:security +/rep:security/rep:authorizables +/rep:security/rep:authorizables/rep:users +/rep:security/rep:authorizables/rep:users/a +/rep:security/rep:authorizables/rep:users/a/ad +/rep:security/rep:authorizables/rep:users/a/ad/admin +/rep:security/rep:authorizables/rep:users/a/an +/rep:security/rep:authorizables/rep:users/a/an/anonymous +/test + +commit / - "test" +commit / - "test2" + +commit / + "test": { "name": "hello" } +commit / + "test2": { "name": "World!" } +commit / + "test3": { "name": "Hallo" } +commit / + "test4": { "name": "10%" } +commit / + "test5": { "name": "10 percent" } + +select name from [nt:base] where name is not null order by upper(name) +10 percent +10% +Hallo +hello +World! + +select * from [nt:base] where length(name) = 5 +/test +/test3 + +select * from [nt:base] where upper(name) = 'HELLO' +/test + +select * from [nt:base] where lower(name) = 'world!' +/test2 + +select * from [nt:base] where name like 'W%' +/test2 + +select * from [nt:base] where name like '%o_%' +/test2 + +select * from [nt:base] where name like '__llo' +/test +/test3 + +select * from [nt:base] where upper(name) like 'H_LLO' +/test +/test3 + +select * from [nt:base] where upper(name) like 'H\_LLO' + +select * from [nt:base] where upper(name) like '10%' +/test4 +/test5 + +select * from [nt:base] where upper(name) like '10\%' +/test4 + diff --git a/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2_explain.txt b/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2_explain.txt new file mode 100644 index 00000000000..60a387c76ed --- /dev/null +++ b/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2_explain.txt @@ -0,0 +1,115 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Syntax: +# * lines starting with "#" are remarks. +# * lines starting with "select" are queries, followed by expected results and an empty line +# * lines starting with "explain" are followed by expected query plan and an empty line +# * lines starting with "sql1" are run using the sql1 language +# * lines starting with "xpath2sql" are just converted from xpath to sql2 +# * all other lines are are committed into the microkernel (line by line) +# * new tests are typically be added on top, after the syntax docs +# * use ascii character only + +# property type (value prefix) index + +commit / + "test": { "a": { "id": "ref:123" }, "b": { "id" : "str:123" }} + +explain select * from [nt:base] where property([*], 'REFERENCE') = CAST('123' AS REFERENCE) +[nt:base] as [nt:base] /* traverse "//*" where property([nt:base].[*], 'reference') = cast('123' as reference) */ + +explain select * from [nt:base] where property(id, 'REFERENCE') = CAST('123' AS REFERENCE) +[nt:base] as [nt:base] /* traverse "//*" where property([nt:base].[id], 'reference') = cast('123' as reference) */ + +select * from [nt:base] where property([*], 'REFERENCE') = CAST('123' AS REFERENCE) +/test/a + +commit /oak:index + "indexes": { "type": "property" } +commit /oak:index/indexes + "prefix@ref:": {} + +explain select * from [nt:base] where property([*], 'REFERENCE') = CAST('123' AS REFERENCE) +[nt:base] as [nt:base] /* prefixIndex "ref:123" where property([nt:base].[*], 'reference') = cast('123' as reference) */ + +explain select * from [nt:base] where property(id, 'REFERENCE') = CAST('123' AS REFERENCE) +[nt:base] as [nt:base] /* prefixIndex "ref:123" where property([nt:base].[id], 'reference') = cast('123' as reference) */ + +select * from [nt:base] where property([*], 'REFERENCE') = CAST('123' AS REFERENCE) +/test/a + +select * from [nt:base] where property(id, 'REFERENCE') = CAST('123' AS REFERENCE) +/test/a + +commit / - "test" +commit /oak:index/indexes - "prefix@ref:" + +# test the property content index + +commit / + "test": { "a": { "id": "10" }, "b": { "id" : "20" }} +commit /oak:index/indexes + "property@id,unique": {} + +# combining 'not' and 'and' + +select * from [nt:base] where id is not null and not id = '100' and id <> '20' +/test/a + +select * from [nt:base] where id is not null and not (id = '100' and id <> '20') +/test/a +/test/b + +explain select * from [nt:base] where id = '10' +[nt:base] as [nt:base] /* propertyIndex "id [10..10]" where [nt:base].[id] = cast('10' as string) */ + +select * from [nt:base] where id = '10' +/test/a + +select [jcr:path], * from [nt:base] where id = '10' +/test/a, null + +explain select * from [nt:base] where id > '10' +[nt:base] as [nt:base] /* traverse "//*" where [nt:base].[id] > cast('10' as string) */ + +commit / - "test" +commit /oak:index/indexes - "property@id,unique" + +# other tests + +commit / + "test": { "jcr:resource": {}, "resource": { "x" : {}}} + +explain select * from [nt:base] as b where ischildnode(b, '/test') +[nt:base] as [b] /* traverse "/test/*" where ischildnode([b], [/test]) */ + +explain select * from [nt:base] as b where isdescendantnode(b, '/test') +[nt:base] as [b] /* traverse "/test//*" where isdescendantnode([b], [/test]) */ + +commit / - "test" + +commit / + "parents": { "p0": {"id": "0"}, "p1": {"id": "1"}, "p2": {"id": "2"}} +commit / + "children": { "c1": {"p": "1"}, "c2": {"p": "1"}, "c3": {"p": "2"}, "c4": {"p": "3"}} + +explain select * from [nt:base] as p inner join [nt:base] as c on p.id = c.p +[nt:base] as [p] /* traverse "//*" where [p].[id] is not null */ inner join [nt:base] as [c] /* traverse "//*" where [c].[p] is not null */ on [p].[id] = [c].[p] + +explain select * from [nt:base] as p inner join [nt:base] as p2 on issamenode(p2, p) where p.[jcr:path] = '/parents' +[nt:base] as [p] /* traverse "//*" where [p].[jcr:path] = cast('/parents' as string) */ inner join [nt:base] as [p2] /* traverse "" */ on issamenode([p2], [p], [.]) + +explain select * from [nt:base] as p inner join [nt:base] as c on p.id = c.p +[nt:base] as [p] /* traverse "//*" where [p].[id] is not null */ inner join [nt:base] as [c] /* traverse "//*" where [c].[p] is not null */ on [p].[id] = [c].[p] + +explain select * from [nt:base] where id = 1 order by id +[nt:base] as [nt:base] /* traverse "//*" where [nt:base].[id] = cast('1' as long) */ + +commit / - "parents" +commit / - "children" + diff --git a/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2_measure.txt b/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2_measure.txt new file mode 100644 index 00000000000..79d78ffbb8c --- /dev/null +++ b/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2_measure.txt @@ -0,0 +1,78 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Syntax: +# * lines starting with "#" are remarks. +# * lines starting with "select" are queries, followed by expected results and an empty line +# * lines starting with "explain" are followed by expected query plan and an empty line +# * lines starting with "sql1" are run using the sql1 language +# * lines starting with "xpath2sql" are just converted from xpath to sql2 +# * all other lines are are committed into the microkernel (line by line) +# * new tests are typically be added on top, after the syntax docs +# * use ascii character only + +commit / + "parents": { "p0": {"id": "0"}, "p1": {"id": "1"}, "p2": {"id": "2"}} +commit / + "children": { "c1": {"p": "1"}, "c2": {"p": "1"}, "c3": {"p": "2"}, "c4": {"p": "3"}} + +measure select * from [nt:base] as c right outer join [nt:base] as p on p.id = c.p where p.id is not null and not isdescendantnode(p, '/jcr:system') +c, 669 +p, 223 +query, 4 + +measure select * from [nt:base] as p left outer join [nt:base] as c on p.id = c.p where p.id is not null +c, 669 +p, 223 +query, 4 + +measure select * from [nt:base] as p left outer join [nt:base] as c on p.id = c.p where p.id is not null and c.p is null +c, 669 +p, 223 +query, 1 + +measure select * from [nt:base] as p left outer join [nt:base] as c on p.id = c.p where p.id is not null and c.p is not null +c, 669 +p, 223 +query, 3 + +measure select * from [nt:base] as p inner join [nt:base] as c on p.id = c.p +c, 669 +p, 223 +query, 3 + +measure select * from [nt:base] as c right outer join [nt:base] as p on p.id = c.p where p.id is not null and not isdescendantnode(p, '/jcr:system') +c, 669 +p, 223 +query, 4 + +measure select * from [nt:base] as p left outer join [nt:base] as c on p.id = c.p where p.id is not null +c, 669 +p, 223 +query, 4 + +measure select * from [nt:base] as p left outer join [nt:base] as c on p.id = c.p where p.id is not null and c.p is null +c, 669 +p, 223 +query, 1 + +measure select * from [nt:base] as p left outer join [nt:base] as c on p.id = c.p where p.id is not null and c.p is not null +c, 669 +p, 223 +query, 3 + +measure select * from [nt:base] as p inner join [nt:base] as c on p.id = c.p +c, 669 +p, 223 +query, 3 + diff --git a/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/xpath.txt b/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/xpath.txt new file mode 100644 index 00000000000..20195e4f48e --- /dev/null +++ b/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/xpath.txt @@ -0,0 +1,379 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Syntax: +# * lines starting with "#" are remarks. +# * lines starting with "select" are queries, followed by expected results and an empty line +# * lines starting with "explain" are followed by expected query plan and an empty line +# * lines starting with "sql1" are run using the sql1 language +# * lines starting with "xpath2sql" are just converted from xpath to sql2 +# * all other lines are are committed into the microkernel (line by line) +# * new tests are typically be added on top, after the syntax docs +# * use ascii character only + +# jackrabbit test queries + +xpath /jcr:root/testroot//*[0] +java.text.ParseException: /jcr:root/testroot//*[0] converted to SQL-2 Query: select [jcr:path], [jcr:score], * from [nt:base] as a where 0(*)is not null and isdescendantnode(a, '/testroot'); expected: NOT, ( + +xpath2sql /test +select [jcr:path], [jcr:score], * from [nt:base] as a where name(a) = 'test' and issamenode(a, '/') + +xpath2sql / +invalid: Query: (*)/; expected: jcr:root, /, *, text, element, @, ( + +xpath2sql /[@name='data'] +invalid: Query: /[(*)@name='data']; expected: jcr:root, /, *, text, element, @, ( + +xpath2sql //[@name='data'] +invalid: Query: //[(*)@name='data']; expected: *, text, element, @, ( + +xpath2sql //child/[@id='2.1'] +invalid: Query: //child/[(*)@id='2.1']; expected: jcr:root, /, *, text, element, @, ( + +xpath2sql // +invalid: Query: /(*)/; expected: *, text, element, @, ( + +xpath2sql [@name='data'] +invalid: Query: [(*)@name='data']; expected: /, *, text, element, @, ( + +xpath2sql test +select [jcr:path], [jcr:score], * from [nt:base] as a where issamenode(a, '/test') + +xpath2sql jcr:root +select [jcr:path], [jcr:score], * from [nt:base] as a where issamenode(a, '/jcr:root') + +xpath2sql /jcr:root +select [jcr:path], [jcr:score], * from [nt:base] as a where issamenode(a, '/') + +xpath2sql //jcr:root +select [jcr:path], [jcr:score], * from [nt:base] as a where name(a) = 'jcr:root' + +xpath2sql * +select [jcr:path], [jcr:score], * from [nt:base] as a where ischildnode(a, '/') + +xpath2sql /* +select [jcr:path], [jcr:score], * from [nt:base] as a where issamenode(a, '/') + +xpath2sql //* +select [jcr:path], [jcr:score], * from [nt:base] as a + +xpath2sql test/* +select [jcr:path], [jcr:score], * from [nt:base] as a where ischildnode(a, '/test') + +xpath2sql element(*, nt:folder) +select [jcr:path], [jcr:score], * from [nt:folder] as a where ischildnode(a, '/') + +xpath2sql //test +select [jcr:path], [jcr:score], * from [nt:base] as a where name(a) = 'test' + +xpath2sql /jcr:root[@foo = 'does-not-exist'] +select [jcr:path], [jcr:score], * from [nt:base] as a where [foo] = 'does-not-exist' and issamenode(a, '/') + +xpath2sql +select [jcr:path], [jcr:score], * from [nt:base] as a where name(a) = 'jcr:root' + +xpath2sql /jcr:root/testroot/*/node11 +select b.[jcr:path] as [jcr:path], b.[jcr:score] as [jcr:score], b.* from [nt:base] as a inner join [nt:base] as b on ischildnode(b, a) where ischildnode(a, '/testroot') and name(b) = 'node11' + +# eq can't currently be supported as there is no equivalent in SQL-2 +# (the behavior is different from = if one of the operands is a multi-valued property) +xpath2sql //testRoot/*[@jcr:primaryType='nt:unstructured' and @text eq 'foo'] +invalid: Query: //testRoot/*[@jcr:primaryType='nt:unstructured' and @text eq(*)'foo']; expected: ] + +xpath2sql //testRoot/*[@text = 'foo'] +select b.[jcr:path] as [jcr:path], b.[jcr:score] as [jcr:score], b.* from [nt:base] as a inner join [nt:base] as b on ischildnode(b, a) where name(a) = 'testRoot' and b.[text] = 'foo' + +xpath2sql /testRoot/*[@jcr:primaryType='nt:unstructured' and fn:not(@mytext)] +select b.[jcr:path] as [jcr:path], b.[jcr:score] as [jcr:score], b.* from [nt:base] as a inner join [nt:base] as b on ischildnode(b, a) where name(a) = 'testRoot' and issamenode(a, '/') and b.[jcr:primaryType] = 'nt:unstructured' and b.[mytext] is null + +xpath2sql /jcr:root/testroot/*[jcr:contains(., '"quick brown" -cat')] +select [jcr:path], [jcr:score], * from [nt:base] as a where contains(*, '"quick brown" -cat') and ischildnode(a, '/testroot') + +xpath2sql //element(*,rep:Authorizable)[(((jcr:contains(profile/givenName,'**') or jcr:contains(profile/familyName,'**')) or jcr:contains(profile/email,'**')) or (jcr:like(rep:principalName,'%%') or jcr:like(fn:name(.),'%%')))] order by rep:principalName ascending +select [jcr:path], [jcr:score], * from [rep:Authorizable] as a where contains([profile/givenName/*], '**') or contains([profile/familyName/*], '**') or contains([profile/email/*], '**') or [rep:principalName/*] like '%%' or name(a) like '%%' order by [rep:principalName/*] + +xpath2sql //element(*,rep:Authorizable)[(((jcr:contains(profile/@givenName,'**') or jcr:contains(profile/@familyName,'**')) or jcr:contains(profile/@email,'**')) or (jcr:like(@rep:principalName,'%%') or jcr:like(fn:name(.),'%%')))] order by @rep:principalName ascending +select [jcr:path], [jcr:score], * from [rep:Authorizable] as a where contains([profile/givenName], '**') or contains([profile/familyName], '**') or contains([profile/email], '**') or [rep:principalName] like '%%' or name(a) like '%%' order by [rep:principalName] + +xpath2sql /jcr:root/testroot//*[jcr:contains(@jcr:data, 'lazy')] +select [jcr:path], [jcr:score], * from [nt:base] as a where contains([jcr:data], 'lazy') and isdescendantnode(a, '/testroot') + +xpath2sql /jcr:root/testroot/*[jcr:contains(jcr:content, 'lazy')] +select [jcr:path], [jcr:score], * from [nt:base] as a where contains([jcr:content/*], 'lazy') and ischildnode(a, '/testroot') + +xpath2sql /jcr:root/testroot/*[jcr:contains(*, 'lazy')] +select [jcr:path], [jcr:score], * from [nt:base] as a where contains([*/*], 'lazy') and ischildnode(a, '/testroot') + +xpath2sql /jcr:root/testroot/*[jcr:contains(*/@jcr:data, 'lazy')] +select [jcr:path], [jcr:score], * from [nt:base] as a where contains([*/jcr:data], 'lazy') and ischildnode(a, '/testroot') + +xpath2sql /jcr:root/testroot/*[jcr:contains(*/@*, 'lazy')] +select [jcr:path], [jcr:score], * from [nt:base] as a where contains([*/*], 'lazy') and ischildnode(a, '/testroot') + +xpath2sql /jcr:root/testroot/*[@prop1 = 1 and jcr:like(fn:name(), 'F%')] +select [jcr:path], [jcr:score], * from [nt:base] as a where [prop1] = 1 and name(a) like 'F%' and ischildnode(a, '/testroot') + +# TODO support rep:excerpt() and rep:similar()? how? +xpath2sql /jcr:root/testroot/*[jcr:contains(., 'jackrabbit')]/rep:excerpt(.) +invalid: Query: /jcr:root/testroot/*[jcr:contains(., 'jackrabbit')]/rep:excerpt((*).); expected: + +xpath2sql /jcr:root/testroot//child/..[@foo1] +invalid: Query: /jcr:root/testroot//child/.(*).[@foo1]; expected: jcr:root, /, *, text, element, @, ( + +xpath2sql //testroot/*[@jcr:primaryType='nt:unstructured' and fn:not(@mytext)] +select b.[jcr:path] as [jcr:path], b.[jcr:score] as [jcr:score], b.* from [nt:base] as a inner join [nt:base] as b on ischildnode(b, a) where name(a) = 'testroot' and b.[jcr:primaryType] = 'nt:unstructured' and b.[mytext] is null + +xpath2sql /jcr:root/testroot/people/jcr:deref(@worksfor, '*') +invalid: Query: /jcr:root/testroot/people/jcr:deref((*)@worksfor, '*'); expected: + +xpath2sql //*[@jcr:primaryType='nt:unstructured' and jcr:like(@foo,"%ar'ba%")] +select [jcr:path], [jcr:score], * from [nt:base] as a where [jcr:primaryType] = 'nt:unstructured' and [foo] like '%ar''ba%' + +xpath2sql /jcr:root/testroot/*[fn:lower-case(@prop1) = 'foo'] +select [jcr:path], [jcr:score], * from [nt:base] as a where lower([prop1]) = 'foo' and ischildnode(a, '/testroot') + +xpath2sql /jcr:root/testroot/*[fn:lower-case(@prop1) != 'foo'] +select [jcr:path], [jcr:score], * from [nt:base] as a where lower([prop1]) <> 'foo' and ischildnode(a, '/testroot') + +xpath2sql /jcr:root/testroot/*[fn:lower-case(@prop1) <= 'foo'] +select [jcr:path], [jcr:score], * from [nt:base] as a where lower([prop1]) <= 'foo' and ischildnode(a, '/testroot') + +xpath2sql /jcr:root/testroot/*[fn:lower-case(@prop1) >= 'foo'] +select [jcr:path], [jcr:score], * from [nt:base] as a where lower([prop1]) >= 'foo' and ischildnode(a, '/testroot') + +xpath2sql /jcr:root/testroot/*[fn:lower-case(@prop1) < 'foo'] +select [jcr:path], [jcr:score], * from [nt:base] as a where lower([prop1]) < 'foo' and ischildnode(a, '/testroot') + +xpath2sql /jcr:root/testroot/*[fn:lower-case(@prop1) > 'foo'] +select [jcr:path], [jcr:score], * from [nt:base] as a where lower([prop1]) > 'foo' and ischildnode(a, '/testroot') + +xpath2sql /jcr:root/testroot/*[fn:lower-case(@prop1) <> 'foo'] +select [jcr:path], [jcr:score], * from [nt:base] as a where lower([prop1]) <> 'foo' and ischildnode(a, '/testroot') + +xpath2sql /jcr:root/testroot/*[@prop1 = 1 and fn:name() = 'node1'] +select [jcr:path], [jcr:score], * from [nt:base] as a where [prop1] = 1 and name(a) = 'node1' and ischildnode(a, '/testroot') + +# sling queries + +xpath2sql //element(*,mix:language)[fn:lower-case(@jcr:language)='en']//element(*,sling:Message)[@sling:message]/(@sling:key|@sling:message) +select b.[jcr:path] as [jcr:path], b.[jcr:score] as [jcr:score], b.[sling:key] as [sling:key], b.[sling:message] as [sling:message] from [mix:language] as a inner join [sling:Message] as b on isdescendantnode(b, a) where lower(a.[jcr:language]) = 'en' and b.[sling:message] is not null + +xpath2sql //element(*,mix:language)[fn:upper-case(@jcr:language)='en']//element(*,sling:Message)[@sling:message]/(@sling:key|@sling:message) +select b.[jcr:path] as [jcr:path], b.[jcr:score] as [jcr:score], b.[sling:key] as [sling:key], b.[sling:message] as [sling:message] from [mix:language] as a inner join [sling:Message] as b on isdescendantnode(b, a) where upper(a.[jcr:language]) = 'en' and b.[sling:message] is not null + +# jboss example queries + +xpath2sql //element(*,my:type) +select [jcr:path], [jcr:score], * from [my:type] as a + +xpath2sql //element(*,my:type)/@my:title +select [jcr:path], [jcr:score], [my:title] from [my:type] as a + +xpath2sql //element(*,my:type)/(@my:title | @my:text) +select [jcr:path], [jcr:score], [my:title], [my:text] from [my:type] as a + +# other queries + +xpath2sql /jcr:root/testdata/node[@jcr:primaryType] +select [jcr:path], [jcr:score], * from [nt:base] as a where [jcr:primaryType] is not null and issamenode(a, '/testdata/node') + +xpath2sql //testroot/*[@jcr:primaryType='nt:unstructured'] order by @prop2, @prop1 +select b.[jcr:path] as [jcr:path], b.[jcr:score] as [jcr:score], b.* from [nt:base] as a inner join [nt:base] as b on ischildnode(b, a) where name(a) = 'testroot' and b.[jcr:primaryType] = 'nt:unstructured' order by b.[prop2], b.[prop1] + +xpath2sql /jcr:root/test//jcr:xmltext +select [jcr:path], [jcr:score], * from [nt:base] as a where name(a) = 'jcr:xmltext' and isdescendantnode(a, '/test') + +xpath2sql /jcr:root/test//text() +select [jcr:path], [jcr:score], * from [nt:base] as a where name(a) = 'jcr:xmltext' and isdescendantnode(a, '/test') + +xpath2sql /jcr:root/test/jcr:xmltext +select [jcr:path], [jcr:score], * from [nt:base] as a where issamenode(a, '/test/jcr:xmltext') + +xpath2sql /jcr:root/test/text() +select [jcr:path], [jcr:score], * from [nt:base] as a where issamenode(a, '/test/jcr:xmltext') + +xpath2sql //*[@name='Hello'] +select [jcr:path], [jcr:score], * from [nt:base] as a where [name] = 'Hello' + +xpath2sql /jcr:root//*[@name='Hello'] +select [jcr:path], [jcr:score], * from [nt:base] as a where [name] = 'Hello' and isdescendantnode(a, '/') + +xpath2sql content/* +select [jcr:path], [jcr:score], * from [nt:base] as a where ischildnode(a, '/content') + +xpath2sql content//* +select [jcr:path], [jcr:score], * from [nt:base] as a where isdescendantnode(a, '/content') + +xpath2sql content//*[@name='Hello'] +select [jcr:path], [jcr:score], * from [nt:base] as a where [name] = 'Hello' and isdescendantnode(a, '/content') + +xpath2sql /jcr:root/content//*[@name='Hello'] +select [jcr:path], [jcr:score], * from [nt:base] as a where [name] = 'Hello' and isdescendantnode(a, '/content') + +xpath2sql //*[jcr:contains(., 'test')] order by @jcr:score +select [jcr:path], [jcr:score], * from [nt:base] as a where contains(*, 'test') order by [jcr:score] + +xpath2sql /jcr:root//*[jcr:contains(., 'test')] order by @jcr:score +select [jcr:path], [jcr:score], * from [nt:base] as a where contains(*, 'test') and isdescendantnode(a, '/') order by [jcr:score] + +xpath2sql /jcr:root//element(*, test) +select [jcr:path], [jcr:score], * from [test] as a where isdescendantnode(a, '/') + +xpath2sql /jcr:root//element(*, user)[test/@jcr:primaryType] +select [jcr:path], [jcr:score], * from [user] as a where [test/jcr:primaryType] is not null and isdescendantnode(a, '/') + +xpath2sql /jcr:root/content//*[(@sling:resourceType = 'start')] +select [jcr:path], [jcr:score], * from [nt:base] as a where [sling:resourceType] = 'start' and isdescendantnode(a, '/content') + +xpath2sql /jcr:root/content//*[(@sling:resourceType = 'page')] +select [jcr:path], [jcr:score], * from [nt:base] as a where [sling:resourceType] = 'page' and isdescendantnode(a, '/content') + +xpath2sql /jcr:root/content//*[@offTime > xs:dateTime('2012-03-28T15:56:18.327+02:00') or @onTime > xs:dateTime('2012-03-28T15:56:18.327+02:00')] +select [jcr:path], [jcr:score], * from [nt:base] as a where ([offTime] > cast('2012-03-28T15:56:18.327+02:00' as date) or [onTime] > cast('2012-03-28T15:56:18.327+02:00' as date)) and isdescendantnode(a, '/content') + +xpath2sql /jcr:root/content/campaigns//*[@jcr:primaryType='Page'] order by jcr:content/@lastModified descending +select [jcr:path], [jcr:score], * from [nt:base] as a where [jcr:primaryType] = 'Page' and isdescendantnode(a, '/content/campaigns') order by [jcr:content/lastModified] desc + +xpath2sql /jcr:root/content/campaigns//element(*, PageContent)[(@sling:resourceType = 'teaser' or @sling:resourceType = 'newsletter' or @teaserPageType = 'newsletter' or @teaserPageType = 'tweet') and ((@onTime < xs:dateTime('2012-04-01T00:00:00.000+02:00')) or not(@onTime)) and ((@offTime >= xs:dateTime('2012-02-26T00:00:00.000+01:00')) or not(@offTime))] order by @onTime +select [jcr:path], [jcr:score], * from [PageContent] as a where ([sling:resourceType] = 'teaser' or [sling:resourceType] = 'newsletter' or [teaserPageType] = 'newsletter' or [teaserPageType] = 'tweet') and ([onTime] < cast('2012-04-01T00:00:00.000+02:00' as date) or [onTime] is null) and ([offTime] >= cast('2012-02-26T00:00:00.000+01:00' as date) or [offTime] is null) and isdescendantnode(a, '/content/campaigns') order by [onTime] + +xpath2sql /jcr:root/content/dam//element(*, asset) +select [jcr:path], [jcr:score], * from [asset] as a where isdescendantnode(a, '/content/dam') + +xpath2sql /jcr:root/content/dam//element(*, asset)[jcr:content/metadata/@dam:scene] +select [jcr:path], [jcr:score], * from [asset] as a where [jcr:content/metadata/dam:scene] is not null and isdescendantnode(a, '/content/dam') + +xpath2sql /jcr:root/etc/cloud//*[(@sling:resourceType = 'framework')] +select [jcr:path], [jcr:score], * from [nt:base] as a where [sling:resourceType] = 'framework' and isdescendantnode(a, '/etc/cloud') + +xpath2sql /jcr:root/etc/cloud//*[(@sling:resourceType = 'analytics')] +select [jcr:path], [jcr:score], * from [nt:base] as a where [sling:resourceType] = 'analytics' and isdescendantnode(a, '/etc/cloud') + +xpath2sql /jcr:root/etc/reports//*[@jcr:primaryType='Page'] order by jcr:content/@lastModified descending +select [jcr:path], [jcr:score], * from [nt:base] as a where [jcr:primaryType] = 'Page' and isdescendantnode(a, '/etc/reports') order by [jcr:content/lastModified] desc + +xpath2sql /jcr:root/etc/segment//*[@jcr:primaryType='Page'] order by jcr:content/@lastModified descending +select [jcr:path], [jcr:score], * from [nt:base] as a where [jcr:primaryType] = 'Page' and isdescendantnode(a, '/etc/segment') order by [jcr:content/lastModified] desc + +xpath2sql /jcr:root/etc/workflow//element(*,Item)[not(meta/@archived) and not(meta/@archived = true)] +select [jcr:path], [jcr:score], * from [Item] as a where [meta/archived] is null and not([meta/archived] = true) and isdescendantnode(a, '/etc/workflow') + +xpath2sql /jcr:root/home//element() +select [jcr:path], [jcr:score], * from [nt:base] as a where isdescendantnode(a, '/home') + +xpath2sql /jcr:root/home//element(*) +select [jcr:path], [jcr:score], * from [nt:base] as a where isdescendantnode(a, '/home') + +# other queries + +xpath2sql //element(*, my:type) +select [jcr:path], [jcr:score], * from [my:type] as a + +xpath2sql //element(*, my:type)/@my:title +select [jcr:path], [jcr:score], [my:title] from [my:type] as a + +xpath2sql //element(*, my:type)/(@my:title | @my:text) +select [jcr:path], [jcr:score], [my:title], [my:text] from [my:type] as a + +xpath2sql /jcr:root/nodes//element(*, my:type) +select [jcr:path], [jcr:score], * from [my:type] as a where isdescendantnode(a, '/nodes') + +xpath2sql /jcr:root/some/element(nodes, my:type) +select [jcr:path], [jcr:score], * from [my:type] as a where issamenode(a, '/some/nodes') + +xpath2sql /jcr:root/some/nodes/element(*, my:type) +select [jcr:path], [jcr:score], * from [my:type] as a where ischildnode(a, '/some/nodes') + +xpath2sql /jcr:root/some/nodes//element(*, my:type) +select [jcr:path], [jcr:score], * from [my:type] as a where isdescendantnode(a, '/some/nodes') + +xpath2sql //element(*, my:type)[@my:title = 'JSR 170'] +select [jcr:path], [jcr:score], * from [my:type] as a where [my:title] = 'JSR 170' + +xpath2sql //element(*, my:type)[jcr:like(@title,'%Java%')] +select [jcr:path], [jcr:score], * from [my:type] as a where [title] like '%Java%' + +xpath2sql //element(*, my:type)[jcr:contains(., 'JSR 170')] +select [jcr:path], [jcr:score], * from [my:type] as a where contains(*, 'JSR 170') + +xpath2sql //element(*, my:type)[@my:title] +select [jcr:path], [jcr:score], * from [my:type] as a where [my:title] is not null + +xpath2sql //element(*, my:type)[not(@my:title)] +select [jcr:path], [jcr:score], * from [my:type] as a where [my:title] is null + +xpath2sql //element(*, my:type)[@my:value < -1.0] +select [jcr:path], [jcr:score], * from [my:type] as a where [my:value] < -1.0 + +xpath2sql //element(*, my:type)[@my:value > +10123123123] +select [jcr:path], [jcr:score], * from [my:type] as a where [my:value] > 10123123123 + +xpath2sql //element(*, my:type)[@my:value <= 10.3e-3] +select [jcr:path], [jcr:score], * from [my:type] as a where [my:value] <= 10.3e-3 + +xpath2sql //element(*, my:type)[@my:value >= 0e3] +select [jcr:path], [jcr:score], * from [my:type] as a where [my:value] >= 0e3 + +xpath2sql //element(*, my:type)[@my:value <> 'Joe''s Caffee'] +select [jcr:path], [jcr:score], * from [my:type] as a where [my:value] <> 'Joe''s Caffee' + +xpath2sql //element(*, my:type)[(not(@my:title) and @my:subject)] +select [jcr:path], [jcr:score], * from [my:type] as a where [my:title] is null and [my:subject] is not null + +xpath2sql //element(*, my:type)[not(@my:title) or @my:subject] +select [jcr:path], [jcr:score], * from [my:type] as a where [my:title] is null or [my:subject] is not null + +xpath2sql //element(*, my:type)[not(@my:value > 0 and @my:value < 100)] +select [jcr:path], [jcr:score], * from [my:type] as a where not([my:value] > 0 and [my:value] < 100) + +xpath2sql //element(*, my:type) order by @jcr:lastModified +select [jcr:path], [jcr:score], * from [my:type] as a order by [jcr:lastModified] + +xpath2sql //element(*, my:type) order by @my:date descending, @my:title ascending +select [jcr:path], [jcr:score], * from [my:type] as a order by [my:date] desc, [my:title] + +xpath2sql //element(*, my:type)[jcr:contains(., 'jcr')] order by jcr:score() descending +select [jcr:path], [jcr:score], * from [my:type] as a where contains(*, 'jcr') order by score(a) desc + +xpath2sql //element(*, my:type)[jcr:contains(@my:title, 'jcr')] order by jcr:score() descending +select [jcr:path], [jcr:score], * from [my:type] as a where contains([my:title], 'jcr') order by score(a) desc + +xpath2sql [invalid/query +invalid: Query: [(*)invalid/query; expected: /, *, text, element, @, ( + +xpath2sql //element(*, my:type)[@my:value = -'x'] +invalid: Query: //element(*, my:type)[@my:value = -'x'(*)] + +xpath2sql //element(-1, my:type) +invalid: Query: //element(-(*)1, my:type); expected: identifier + +xpath2sql //element(*, my:type)[not @my:title] +invalid: Query: //element(*, my:type)[not @(*)my:title]; expected: ( + +xpath2sql //element(*, my:type)[@my:value = +'x'] +invalid: Query: //element(*, my:type)[@my:value = +'x'(*)] + +xpath2sql //element(*, my:type)[@my:value = ['x'] +invalid: Query: //element(*, my:type)[@my:value = [(*)'x']; expected: @, true, false, -, +, *, ., @, ( + +xpath2sql //element(*, my:type)[jcr:strike(@title,'%Java%')] +invalid: Query: //element(*, my:type)[jcr:strike(@(*)title,'%Java%')]; expected: jcr:like | jcr:contains | jcr:score | xs:dateTime | fn:lower-case | fn:upper-case | fn:name + +xpath2sql //element(*, my:type)[ +invalid: Query: //element(*, my:type)(*)[; expected: fn:not, not, (, @, true, false, -, +, *, ., @, ( + +xpath2sql //element(*, my:type)[@my:value >= %] +invalid: Query: //element(*, my:type)[@my:value >= %(*)]; expected: @, true, false, -, +, *, ., @, ( diff --git a/oak-core/src/test/resources/org/apache/jackrabbit/mk/json/test.json b/oak-core/src/test/resources/org/apache/jackrabbit/oak/util/test.json similarity index 100% rename from oak-core/src/test/resources/org/apache/jackrabbit/mk/json/test.json rename to oak-core/src/test/resources/org/apache/jackrabbit/oak/util/test.json diff --git a/oak-http/pom.xml b/oak-http/pom.xml new file mode 100644 index 00000000000..795d1162505 --- /dev/null +++ b/oak-http/pom.xml @@ -0,0 +1,124 @@ + + + + + + 4.0.0 + + + org.apache.jackrabbit + oak-parent + 0.6-SNAPSHOT + ../oak-parent/pom.xml + + + oak-http + Oak HTTP Binding + bundle + + + + + org.apache.felix + maven-bundle-plugin + + + + ! + + + org.apache.jackrabbit.oak.http.osgi.Activator + + + + + + + + + org.apache.rat + apache-rat-plugin + + + + + + + + + + + + org.osgi + org.osgi.core + provided + + + org.osgi + org.osgi.compendium + provided + + + + org.apache.jackrabbit + oak-core + ${project.version} + + + org.apache.tika + tika-core + 1.1 + + + com.fasterxml.jackson.core + jackson-core + 2.0.0 + + + com.fasterxml.jackson.core + jackson-databind + 2.0.0 + + + com.fasterxml.jackson.dataformat + jackson-dataformat-smile + 2.0.2 + + + javax.servlet + servlet-api + 2.5 + provided + + + + + com.google.code.findbugs + jsr305 + 2.0.0 + provided + + + + + junit + junit + test + + + diff --git a/oak-http/src/main/java/org/apache/jackrabbit/oak/http/AcceptHeader.java b/oak-http/src/main/java/org/apache/jackrabbit/oak/http/AcceptHeader.java new file mode 100644 index 00000000000..158700e3a2f --- /dev/null +++ b/oak-http/src/main/java/org/apache/jackrabbit/oak/http/AcceptHeader.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.http; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.tika.mime.MediaType; +import org.apache.tika.mime.MediaTypeRegistry; + +import static com.google.common.base.Preconditions.checkArgument; + +public class AcceptHeader { + + private static final MediaTypeRegistry registry = + MediaTypeRegistry.getDefaultRegistry(); + + private final List ranges = new ArrayList(); + + public AcceptHeader(String accept) { + if (accept == null) { + ranges.add(new MediaRange(MediaType.parse("*/*"), 1.0)); + } else { + for (String part : accept.split("(\\s*,)+\\s*")) { + MediaRange range = MediaRange.parse(part, registry); + if (range != null) { + ranges.add(range); + } + } + } + + } + + public Representation resolve(Representation... representations) { + checkArgument(representations != null && representations.length > 0); + int maxIndex = 0; + double maxQ = 0.0; + for (int i = 0; i < representations.length; i++) { + double q = 0.0; + MediaType type = registry.normalize(representations[i].getType()); + for (MediaRange range : ranges) { + q = Math.max(q, range.match(type, registry)); + } + if (q > maxQ) { + maxIndex = i; + maxQ = q; + } + } + return representations[maxIndex]; + } + +} diff --git a/oak-http/src/main/java/org/apache/jackrabbit/oak/http/JsonRepresentation.java b/oak-http/src/main/java/org/apache/jackrabbit/oak/http/JsonRepresentation.java new file mode 100644 index 00000000000..e31b78bac85 --- /dev/null +++ b/oak-http/src/main/java/org/apache/jackrabbit/oak/http/JsonRepresentation.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.http; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; + +import javax.jcr.PropertyType; +import javax.servlet.http.HttpServletResponse; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.tika.mime.MediaType; + +import static org.apache.jackrabbit.oak.api.Type.BINARIES; +import static org.apache.jackrabbit.oak.api.Type.BOOLEANS; +import static org.apache.jackrabbit.oak.api.Type.DECIMALS; +import static org.apache.jackrabbit.oak.api.Type.DOUBLES; +import static org.apache.jackrabbit.oak.api.Type.LONGS; +import static org.apache.jackrabbit.oak.api.Type.STRINGS; + +class JsonRepresentation implements Representation { + + private final MediaType type; + + private final JsonFactory factory; + + public JsonRepresentation(MediaType type, JsonFactory factory) { + this.type = type; + this.factory = factory; + } + + @Override + public MediaType getType() { + return type; + } + + @Override + public void render(Tree tree, HttpServletResponse response) + throws IOException { + JsonGenerator generator = startResponse(response); + render(tree, generator); + generator.close(); + } + + @Override + public void render(PropertyState property, HttpServletResponse response) + throws IOException { + JsonGenerator generator = startResponse(response); + render(property, generator); + generator.close(); + } + + protected JsonGenerator startResponse(HttpServletResponse response) + throws IOException { + response.setContentType(type.toString()); + return factory.createJsonGenerator(response.getOutputStream()); + } + + private static void render(Tree tree, JsonGenerator generator) + throws IOException { + generator.writeStartObject(); + for (PropertyState property : tree.getProperties()) { + generator.writeFieldName(property.getName()); + render(property, generator); + } + for (Tree child : tree.getChildren()) { + generator.writeFieldName(child.getName()); + generator.writeStartObject(); + generator.writeEndObject(); + } + generator.writeEndObject(); + } + + private static void render(PropertyState property, JsonGenerator generator) + throws IOException { + if (property.isArray()) { + generator.writeStartArray(); + renderValue(property, generator); + generator.writeEndArray(); + } else { + renderValue(property, generator); + } + } + + private static void renderValue(PropertyState property, JsonGenerator generator) + throws IOException { + // TODO: Type info? + int type = property.getType().tag(); + if (type == PropertyType.BOOLEAN) { + for (boolean value : property.getValue(BOOLEANS)) { + generator.writeBoolean(value); + } + } else if (type == PropertyType.DECIMAL) { + for (BigDecimal value : property.getValue(DECIMALS)) { + generator.writeNumber(value); + } + } else if (type == PropertyType.DOUBLE) { + for (double value : property.getValue(DOUBLES)) { + generator.writeNumber(value); + } + } else if (type == PropertyType.LONG) { + for (long value : property.getValue(LONGS)) { + generator.writeNumber(value); + } + } else if (type == PropertyType.BINARY) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + for (Blob value : property.getValue(BINARIES)) { + InputStream stream = value.getNewStream(); + try { + byte[] b = new byte[1024]; + int n = stream.read(b); + while (n != -1) { + buffer.write(b, 0, n); + n = stream.read(b); + } + } finally { + stream.close(); + } + generator.writeBinary(buffer.toByteArray()); + } + } else { + for (String value : property.getValue(STRINGS)) { + generator.writeString(value); + } + } + } + +} diff --git a/oak-http/src/main/java/org/apache/jackrabbit/oak/http/MediaRange.java b/oak-http/src/main/java/org/apache/jackrabbit/oak/http/MediaRange.java new file mode 100644 index 00000000000..05e875fe749 --- /dev/null +++ b/oak-http/src/main/java/org/apache/jackrabbit/oak/http/MediaRange.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.http; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.tika.mime.MediaType; +import org.apache.tika.mime.MediaTypeRegistry; + +public class MediaRange { + + private final MediaType type; + + private final double q; + + public static MediaRange parse(String range, MediaTypeRegistry registry) { + MediaType type = MediaType.parse(range); + if (type == null) { + return null; + } + type = registry.normalize(type); + + Map parameters = + new HashMap(type.getParameters()); + String q = parameters.remove("q"); + if (q != null) { + try { + return new MediaRange( + new MediaType(type.getBaseType(), parameters), + Double.parseDouble(q)); + } catch (NumberFormatException e) { + return null; + } + } + + return new MediaRange(type, 1.0); + } + + public MediaRange(MediaType type, double q) { + this.type = type; + this.q = q; + } + + public double match(MediaType type, MediaTypeRegistry registry) { + if (type.equals(this.type)) { // shortcut + return q; + } + + for (Map.Entry entry + : this.type.getParameters().entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (!value.equals(type.getParameters().get(key))) { + return 0.0; + } + } + + if ("*/*".equals(this.type.toString())) { + return q; + } else if ("*".equals(this.type.getSubtype()) + && type.getType().equals(this.type.getType())) { + return q; + } else { + return 0.0; + } + } + +} diff --git a/oak-http/src/main/java/org/apache/jackrabbit/oak/http/OakServlet.java b/oak-http/src/main/java/org/apache/jackrabbit/oak/http/OakServlet.java new file mode 100644 index 00000000000..581f9430252 --- /dev/null +++ b/oak-http/src/main/java/org/apache/jackrabbit/oak/http/OakServlet.java @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.http; + +import java.io.IOException; +import java.util.Iterator; +import java.util.Map.Entry; + +import javax.jcr.GuestCredentials; +import javax.jcr.NoSuchWorkspaceException; +import javax.security.auth.login.LoginException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.smile.SmileFactory; +import org.apache.jackrabbit.oak.api.CommitFailedException; +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.tika.mime.MediaType; + +public class OakServlet extends HttpServlet { + + private static final MediaType JSON = + MediaType.parse("application/json"); + + private static final MediaType SMILE = + MediaType.parse("application/x-jackson-smile"); + + private static final Representation[] REPRESENTATIONS = { + new JsonRepresentation(JSON, new JsonFactory()), + new JsonRepresentation(SMILE, new SmileFactory()), + new PostRepresentation(), + new TextRepresentation() }; + + private final ContentRepository repository; + + public OakServlet(ContentRepository repository) { + this.repository = repository; + } + + @Override + protected void service( + HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + try { + ContentSession session = repository.login(new GuestCredentials(), null); + try { + Root root = session.getLatestRoot(); + Tree tree = root.getTree(request.getPathInfo()); + request.setAttribute("root", root); + request.setAttribute("tree", tree); + super.service(request, response); + } finally { + session.close(); + } + } catch (NoSuchWorkspaceException e) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + } catch (LoginException e) { + throw new ServletException(e); + } + } + + @Override + protected void doGet( + HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + AcceptHeader accept = new AcceptHeader(request.getHeader("Accept")); + Representation representation = accept.resolve(REPRESENTATIONS); + + Tree tree = (Tree) request.getAttribute("tree"); + representation.render(tree, response); + } + + @Override + protected void doPost( + HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + try { + Root root = (Root) request.getAttribute("root"); + Tree tree = (Tree) request.getAttribute("tree"); + ObjectMapper mapper = new ObjectMapper(); + JsonNode node = mapper.readTree(request.getInputStream()); + if (node.isObject()) { + post(node, tree); + root.commit(); + doGet(request, response); + } else { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + } + } catch (CommitFailedException e) { + throw new ServletException(e); + } + } + + private static void post(JsonNode node, Tree tree) { + Iterator> iterator = node.fields(); + while (iterator.hasNext()) { + Entry entry = iterator.next(); + String name = entry.getKey(); + JsonNode value = entry.getValue(); + if (value.isObject()) { + if (tree.hasProperty(name)) { + tree.removeProperty(name); + } + Tree child = tree.getChild(name); + if (child == null) { + child = tree.addChild(name); + } + post(value, child); + } else { + Tree child = tree.getChild(name); + if (child != null) { + child.remove(); + } + if (value.isNull()) { + tree.removeProperty(name); + } else if (value.isBoolean()) { + tree.setProperty(name, value.asBoolean()); + } else if (value.isLong()) { + tree.setProperty(name, value.asLong()); + } else if (value.isDouble()) { + tree.setProperty(name, value.asDouble()); + } else if (value.isBigDecimal()) { + tree.setProperty(name, value.decimalValue()); + } else { + tree.setProperty(name, value.asText()); + } + } + } + } + + @Override + protected void doDelete( + HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + try { + Root root = (Root) request.getAttribute("root"); + Tree tree = (Tree) request.getAttribute("tree"); + Tree parent = tree.getParent(); + if (parent != null) { + Tree child = parent.getChild(tree.getName()); + if (child != null) { + child.remove(); + } + root.commit(); + response.sendError(HttpServletResponse.SC_OK); + } else { + // Can't remove the root node + response.sendError(HttpServletResponse.SC_FORBIDDEN); + } + } catch (CommitFailedException e) { + throw new ServletException(e); + } + } + +} diff --git a/oak-http/src/main/java/org/apache/jackrabbit/oak/http/PostRepresentation.java b/oak-http/src/main/java/org/apache/jackrabbit/oak/http/PostRepresentation.java new file mode 100644 index 00000000000..e8df151dd37 --- /dev/null +++ b/oak-http/src/main/java/org/apache/jackrabbit/oak/http/PostRepresentation.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.http; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.tika.mime.MediaType; + +import static org.apache.jackrabbit.oak.api.Type.STRING; +import static org.apache.jackrabbit.oak.api.Type.STRINGS; + +class PostRepresentation implements Representation { + + private static final MediaType TYPE = + MediaType.parse("application/x-www-form-urlencoded"); + + private static final String ENCODING = "UTF-8"; + + @Override + public MediaType getType() { + return TYPE; + } + + @Override + public void render(Tree tree, HttpServletResponse response) + throws IOException { + PrintWriter writer = startResponse(response); + + boolean first = true; + for (PropertyState property : tree.getProperties()) { + String name = property.getName(); + if (property.isArray()) { + for (String value : property.getValue(STRINGS)) { + first = render(first, name, value, writer); + } + } else { + first = render(first, name, property.getValue(STRING), writer); + } + } + } + + @Override + public void render(PropertyState property, HttpServletResponse response) + throws IOException { + PrintWriter writer = startResponse(response); + if (property.isArray()) { + for (String value : property.getValue(STRINGS)) { + render(value, writer); + writer.print('\n'); + } + } else { + render(property.getValue(STRING), writer); + } + } + + private static PrintWriter startResponse(HttpServletResponse response) + throws IOException { + response.setContentType(TYPE.toString()); + response.setCharacterEncoding(ENCODING); + return response.getWriter(); + } + + private static boolean render( + boolean first, String name, String value, PrintWriter writer) { + if (!first) { + writer.print('&'); + } + render(name, writer); + writer.print('='); + render(value, writer); + return false; + } + + private static void render(String string, PrintWriter writer) { + try { + writer.print(URLEncoder.encode(string, ENCODING)); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/json/JsopWriter.java b/oak-http/src/main/java/org/apache/jackrabbit/oak/http/Representation.java similarity index 61% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/json/JsopWriter.java rename to oak-http/src/main/java/org/apache/jackrabbit/oak/http/Representation.java index d75fae12cf9..7fa9d62b58b 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/json/JsopWriter.java +++ b/oak-http/src/main/java/org/apache/jackrabbit/oak/http/Representation.java @@ -14,36 +14,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.json; +package org.apache.jackrabbit.oak.http; -public interface JsopWriter { +import java.io.IOException; - JsopWriter array(); +import javax.servlet.http.HttpServletResponse; - JsopWriter object(); +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.tika.mime.MediaType; - JsopWriter key(String key); +public interface Representation { - JsopWriter value(String value); + MediaType getType(); - JsopWriter encodedValue(String raw); + void render(Tree tree, HttpServletResponse response) + throws IOException; - JsopWriter endObject(); - - JsopWriter endArray(); - - JsopWriter tag(char tag); - - JsopWriter append(JsopWriter diff); - - JsopWriter value(long x); - - JsopWriter value(boolean b); - - JsopWriter newline(); - - void resetWriter(); - - void setLineLength(int i); + void render(PropertyState property, HttpServletResponse response) + throws IOException; } diff --git a/oak-http/src/main/java/org/apache/jackrabbit/oak/http/TextRepresentation.java b/oak-http/src/main/java/org/apache/jackrabbit/oak/http/TextRepresentation.java new file mode 100644 index 00000000000..edb9af8f5b5 --- /dev/null +++ b/oak-http/src/main/java/org/apache/jackrabbit/oak/http/TextRepresentation.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.http; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.tika.mime.MediaType; + +import static org.apache.jackrabbit.oak.api.Type.STRING; +import static org.apache.jackrabbit.oak.api.Type.STRINGS; + +class TextRepresentation implements Representation { + + @Override + public MediaType getType() { + return MediaType.TEXT_PLAIN; + } + + public void render(Tree tree, HttpServletResponse response) + throws IOException { + PrintWriter writer = startResponse(response); + + for (PropertyState property : tree.getProperties()) { + writer.print(property.getName()); + writer.print(": "); + if (property.isArray()) { + for (String value : property.getValue(STRINGS)) { + writer.print(value); + writer.print(", "); + } + } else { + writer.print(property.getValue(STRING)); + } + writer.print('\n'); + } + + writer.print('\n'); + + for (Tree child : tree.getChildren()) { + writer.print(child.getName()); + writer.print(" -> "); + writer.print(response.encodeRedirectURL(child.getName())); + } + } + + public void render(PropertyState property, HttpServletResponse response) + throws IOException { + PrintWriter writer = startResponse(response); + if (property.isArray()) { + for (String value : property.getValue(Type.STRINGS)) { + writer.print(value); + writer.print('\n'); + } + } else { + writer.print(property.getValue(STRING)); + } + } + + private PrintWriter startResponse(HttpServletResponse response) + throws IOException { + response.setContentType(MediaType.TEXT_PLAIN.toString()); + response.setCharacterEncoding("UTF-8"); + return response.getWriter(); + } + +} diff --git a/oak-http/src/test/java/org/apache/jackrabbit/oak/http/AcceptHeaderTest.java b/oak-http/src/test/java/org/apache/jackrabbit/oak/http/AcceptHeaderTest.java new file mode 100644 index 00000000000..3be146b0c1e --- /dev/null +++ b/oak-http/src/test/java/org/apache/jackrabbit/oak/http/AcceptHeaderTest.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.http; + +import static junit.framework.Assert.assertEquals; + +import org.junit.Test; + +public class AcceptHeaderTest { + +// @Test +// public void testRfcExample1() { +// AcceptHeader accept = new AcceptHeader( +// "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"); +// +// assertEquals("text/plain", accept.resolve("text/plain")); +// assertEquals("text/html", accept.resolve("text/html")); +// assertEquals("text/x-dvi", accept.resolve("text/x-dvi")); +// assertEquals("text/x-c", accept.resolve("text/x-c")); +// +// assertEquals( +// "application/octet-stream", +// accept.resolve("application/octet-stream")); +// assertEquals( +// "application/octet-stream", +// accept.resolve("application/pdf")); +// +// assertEquals("text/html", accept.resolve("text/plain", "text/html")); +// assertEquals("text/x-c", accept.resolve("text/x-dvi", "text/x-c")); +// assertEquals("text/x-dvi", accept.resolve("text/x-dvi", "text/plain")); +// +// assertEquals("text/html", accept.resolve("text/html", "text/x-c")); +// assertEquals("text/x-c", accept.resolve("text/x-c", "text/html")); +// } + +} diff --git a/oak-it/mk/README.md b/oak-it/mk/README.md new file mode 100644 index 00000000000..e2bee8b49f9 --- /dev/null +++ b/oak-it/mk/README.md @@ -0,0 +1,73 @@ +MicroKernel integration tests +============================= + +This component contains integration tests for the `MicroKernel` interface +as defined in the `oak-mk` component. The test suite is by default executed +against the default MicroKernel implementation included in that same +component, but you can also use this test suite against any other +implementations as described below. + +Testing a MicroKernel implementation +------------------------------------ + +Follow these four steps to to set up this integration test suite against +a particular MicroKernel implementation. + +First, you need to add this `oak-it-mk` component as a test dependency +in the component that contains your MicroKernel implementation: + + + org.apache.jackrabbit + oak-it-mk + ... + test + + +Second, you need a JUnit test class that runs the full suite of MicroKernel +tests included in this component: + + import org.junit.runner.RunWith; + import org.junit.runners.Suite; + + import org.apache.jackrabbit.mk.test.MicroKernelTestSuite; + + @RunWith(Suite.class) + @Suite.SuiteClasses({ MicroKernelTestSuite.class }) + public class EverythingIT { + } + +Third, you need to implement the `MicroKernelFixture` interface in a class +with a public default constructor: + + package my.package; + public class MyCustomMicroKernelFixture implements MicroKernelFixture { + ... + } + +Fourth, and finally, you need to list this fixture class in a +`org.apache.jackrabbit.mk.test.MicroKernelFixture` file within the +`META-INF/services/` folder inside your test classpath: + + my.package.MyCustomMicroKernelFixture + +License +------- + +(see the top-level [LICENSE.txt](../LICENSE.txt) for full license details) + +Collective work: Copyright 2012 The Apache Software Foundation. + +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/oak-it/mk/pom.xml b/oak-it/mk/pom.xml new file mode 100644 index 00000000000..31e348b9907 --- /dev/null +++ b/oak-it/mk/pom.xml @@ -0,0 +1,87 @@ + + + + + + 4.0.0 + + + org.apache.jackrabbit + oak-parent + 0.6-SNAPSHOT + ../../oak-parent/pom.xml + + + oak-it-mk + Oak Integration Tests for MicroKernel implementations + + + + + + org.apache.rat + apache-rat-plugin + + + README.md + + + + + + + + + + junit + junit + + + org.apache.jackrabbit + oak-mk-api + ${project.version} + + + org.apache.jackrabbit + oak-mk + ${project.version} + + + org.apache.jackrabbit + oak-mk-remote + ${project.version} + + + com.googlecode.json-simple + json-simple + 1.1 + + + ch.qos.logback + logback-classic + 1.0.1 + test + + + com.google.guava + guava + ${guava.version} + + + + diff --git a/oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/AbstractMicroKernelIT.java b/oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/AbstractMicroKernelIT.java new file mode 100644 index 00000000000..e1ffd4cce7b --- /dev/null +++ b/oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/AbstractMicroKernelIT.java @@ -0,0 +1,392 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.junit.After; +import org.junit.Before; +import org.junit.runners.Parameterized.Parameters; + +import static com.google.common.base.Preconditions.checkArgument; +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; + +/** + * Abstract base class for {@link MicroKernel} integration tests. + */ +public abstract class AbstractMicroKernelIT { + + /** + * Finds and returns all {@link MicroKernelFixture} services available + * in the current classpath. + * + * @return available {@link MicroKernelFixture} services + */ + @Parameters + public static Collection loadFixtures() { + Collection fixtures = new ArrayList(); + + Class iface = MicroKernelFixture.class; + ServiceLoader loader = + ServiceLoader.load(iface, iface.getClassLoader()); + + for (MicroKernelFixture fixture : loader) { + fixtures.add(new Object[] { fixture }); + } + + return fixtures; + } + + /** + * The {@link MicroKernelFixture} service used by this test case instance. + */ + protected final MicroKernelFixture fixture; + + /** + * The {@link MicroKernel} cluster node instances used by this test case. + */ + protected final MicroKernel[] mks; + + /** + * The {@link MicroKernel} instance used by this test case. + * In a clustered setup this is the first node of the cluster. + */ + protected MicroKernel mk; + + /** + * A JSON parser instance that can be used for parsing JSON-format data; + * {@code JSONParser} instances are not not thread-safe. + * @see #getJSONParser() + */ + private JSONParser parser; + + /** + * Creates a {@link MicroKernel} test case for a cluster of the given + * size created using the given {@link MicroKernelFixture} service. + * + * @param fixture {@link MicroKernelFixture} service + * @param nodeCount number of nodes that the test cluster should contain + */ + protected AbstractMicroKernelIT(MicroKernelFixture fixture, int nodeCount) { + checkArgument(nodeCount > 0); + this.fixture = fixture; + this.mks = new MicroKernel[nodeCount]; + } + + /** + * Prepares the test case by initializing the {@link #mks} and + * {@link #mk} variables with a new {@link MicroKernel} cluster + * from the {@link MicroKernelFixture} service associated with + * this test case. + */ + @Before + public void setUp() { + fixture.setUpCluster(mks); + mk = mks[0]; + addInitialTestContent(); + } + + /** + * Adds initial content used by the test case. This method is + * called by the {@link #setUp()} method after the {@link MicroKernel} + * cluster has been set up and before the actual test is run. + * The default implementation does nothing, but subclasses can + * override this method to perform extra initialization. + */ + protected void addInitialTestContent() { + } + + /** + * Releases the {@link MicroKernel} cluster used by this test case. + */ + @After + public void tearDown() { + fixture.tearDownCluster(mks); + + // Clear fields to avoid consuming memory after the test has run. + // It looks like JUnit keeps references to all test instances until + // the entire test suite has been run. + Arrays.fill(mks, null); + mk = null; + parser = null; + } + + //--------------------------------< utility methods for parsing json data > + /** + * Returns a {@code JSONParser} instance for parsing JSON format data. + * This method returns a cached instance. + *

    + * {@code JSONParser} instances are not thread-safe. Multi-threaded + * unit tests should therefore override this method and return a fresh + * instance on every invocation. + * + * @return a {@code JSONParser} instance + */ + protected synchronized JSONParser getJSONParser() { + if (parser == null) { + parser = new JSONParser(); + } + return parser; + } + + /** + * Parses the provided string into a {@code JSONObject}. + * + * @param json string to be parsed + * @return a {@code JSONObject} + * @throws {@code AssertionError} if the string cannot be parsed into a {@code JSONObject} + */ + protected JSONObject parseJSONObject(String json) throws AssertionError { + JSONParser parser = getJSONParser(); + try { + Object obj = parser.parse(json); + assertTrue(obj instanceof JSONObject); + return (JSONObject) obj; + } catch (Exception e) { + throw new AssertionError("not a valid JSON object: " + e.getMessage()); + } + } + + /** + * Parses the provided string into a {@code JSONArray}. + * + * @param json string to be parsed + * @return a {@code JSONArray} + * @throws {@code AssertionError} if the string cannot be parsed into a {@code JSONArray} + */ + protected JSONArray parseJSONArray(String json) throws AssertionError { + JSONParser parser = getJSONParser(); + try { + Object obj = parser.parse(json); + assertTrue(obj instanceof JSONArray); + return (JSONArray) obj; + } catch (Exception e) { + throw new AssertionError("not a valid JSON array: " + e.getMessage()); + } + } + + protected Set getNodeNames(JSONObject obj) { + Set names = new HashSet(); + Set entries = obj.entrySet(); + for (Map.Entry entry : entries) { + if (entry.getValue() instanceof JSONObject) { + names.add((String) entry.getKey()); + } + } + return names; + } + + protected Set getPropertyNames(JSONObject obj) { + Set names = new HashSet(); + Set entries = obj.entrySet(); + for (Map.Entry entry : entries) { + if (! (entry.getValue() instanceof JSONObject)) { + names.add((String) entry.getKey()); + } + } + return names; + } + + protected void assertPropertyExists(JSONObject obj, String relPath) + throws AssertionError { + Object val = resolveValue(obj, relPath); + assertNotNull("not found: " + relPath, val); + } + + protected void assertPropertyNotExists(JSONObject obj, String relPath) + throws AssertionError { + Object val = resolveValue(obj, relPath); + assertNull(val); + } + + protected void assertPropertyExists(JSONObject obj, String relPath, Class type) + throws AssertionError { + Object val = resolveValue(obj, relPath); + assertNotNull("not found: " + relPath, val); + + assertTrue(type.isInstance(val)); + } + + protected void assertPropertyValue(JSONObject obj, String relPath, Double expected) + throws AssertionError { + Object val = resolveValue(obj, relPath); + assertNotNull("not found: " + relPath, val); + + assertEquals(expected, val); + } + + protected void assertPropertyValue(JSONObject obj, String relPath, Long expected) + throws AssertionError { + Object val = resolveValue(obj, relPath); + assertNotNull("not found: " + relPath, val); + + assertEquals(expected, val); + } + + protected void assertPropertyValue(JSONObject obj, String relPath, Boolean expected) + throws AssertionError { + Object val = resolveValue(obj, relPath); + assertNotNull("not found: " + relPath, val); + + assertEquals(expected, val); + } + + protected void assertPropertyValue(JSONObject obj, String relPath, String expected) + throws AssertionError { + Object val = resolveValue(obj, relPath); + assertNotNull("not found: " + relPath, val); + + assertEquals(expected, val); + } + + protected void assertPropertyValue(JSONObject obj, String relPath, Object[] expected) + throws AssertionError { + JSONArray array = resolveArrayValue(obj, relPath); + assertNotNull("not found: " + relPath, array); + + assertEquals(expected.length, array.size()); + + // JSON numeric types: Double, Long + // convert types as necessary for comparison using equals method + for (int i = 0; i < array.size(); i++) { + Object o1 = expected[i]; + Object o2 = array.get(i); + if (o1 instanceof Number && o2 instanceof Number) { + if (o1 instanceof Integer) { + o1 = new Long((Integer) o1); + } else if (o1 instanceof Short) { + o1 = new Long((Short) o1); + } else if (o1 instanceof Float) { + o1 = new Double((Float) o1); + } + } + assertEquals(o1, o2); + } + } + + protected JSONObject resolveObjectValue(JSONObject obj, String relPath) { + Object val = resolveValue(obj, relPath); + if (val instanceof JSONObject) { + return (JSONObject) val; + } + throw new AssertionError("failed to resolve JSONObject value at " + relPath + ": " + val); + } + + protected JSONObject getObjectArrayEntry(JSONArray array, int pos) { + assertTrue(pos >= 0 && pos < array.size()); + Object entry = array.get(pos); + if (entry instanceof JSONObject) { + return (JSONObject) entry; + } + throw new AssertionError("failed to resolve JSONObject array entry at pos " + pos + ": " + entry); + } + + protected JSONArray resolveArrayValue(JSONObject obj, String relPath) { + Object val = resolveValue(obj, relPath); + if (val instanceof JSONArray) { + return (JSONArray) val; + } + throw new AssertionError("failed to resolve JSONArray value at " + relPath + ": " + val); + } + + protected Object resolveValue(JSONObject obj, String relPath) { + String names[] = relPath.split("/"); + Object val = obj; + for (String name : names) { + if (! (val instanceof JSONObject)) { + throw new AssertionError("not found: " + relPath); + } + val = ((JSONObject) val).get(name); + } + return val; + } + + protected void assertPropExists(String rev, String path, String property) { + String nodes = mk.getNodes(path, rev, -1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyExists(obj, property); + } + + protected void assertPropNotExists(String rev, String path, String property) { + String nodes = mk.getNodes(path, rev, -1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + if (nodes == null) { + return; + } + JSONObject obj = parseJSONObject(nodes); + assertPropertyNotExists(obj, property); + } + + protected void assertPropValue(String rev, String path, String property, String value) { + String nodes = mk.getNodes(path, rev, -1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyValue(obj, property, value); + } + + protected String addNodes(String rev, String...nodes) { + String newRev = rev; + for (String node : nodes) { + newRev = mk.commit("", "+\"" + node + "\":{}", newRev, ""); + } + return newRev; + } + + protected String removeNodes(String rev, String...nodes) { + String newRev = rev; + for (String node : nodes) { + newRev = mk.commit("", "-\"" + node + "\"", newRev, ""); + } + return newRev; + } + + protected String setProp(String rev, String prop, Object value) { + value = value == null? null : "\"" + value + "\""; + return mk.commit("", "^\"" + prop + "\" : " + value, rev, ""); + } + + protected void assertNodesExist(String revision, String...paths) { + doAssertNodes(true, revision, paths); + } + + protected void assertNodesNotExist(String revision, String...paths) { + doAssertNodes(false, revision, paths); + } + + protected void doAssertNodes(boolean checkExists, String revision, String...paths) { + for (String path : paths) { + boolean exists = mk.nodeExists(path, revision); + if (checkExists) { + assertTrue(path + " does not exist", exists); + } else { + assertFalse(path + " should not exist", exists); + } + } + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/blobs/DataStoreTest.java b/oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/DataStoreIT.java similarity index 95% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/blobs/DataStoreTest.java rename to oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/DataStoreIT.java index 84c29a9f471..cb04fbc6f22 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/blobs/DataStoreTest.java +++ b/oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/DataStoreIT.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.blobs; +package org.apache.jackrabbit.mk.test; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -22,7 +22,6 @@ import java.io.InputStream; import java.util.Random; import junit.framework.Assert; -import org.apache.jackrabbit.mk.MultiMkTestBase; import org.apache.jackrabbit.mk.util.MicroKernelInputStream; import org.junit.Test; import org.junit.runner.RunWith; @@ -32,10 +31,10 @@ * Test the data store using the MicroKernel API. */ @RunWith(Parameterized.class) -public class DataStoreTest extends MultiMkTestBase { +public class DataStoreIT extends AbstractMicroKernelIT { - public DataStoreTest(String url) { - super(url); + public DataStoreIT(MicroKernelFixture fixture) { + super(fixture, 1); } @Test diff --git a/oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/MicroKernelFixture.java b/oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/MicroKernelFixture.java new file mode 100644 index 00000000000..e74be381a00 --- /dev/null +++ b/oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/MicroKernelFixture.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.test; + +import org.apache.jackrabbit.mk.api.MicroKernel; + +/** + * Interface through which different {@link MicroKernel} implementations + * make themselves available for use by this integration test suite. + */ +public interface MicroKernelFixture { + + /** + * Creates a new {@link MicroKernel} cluster with as many nodes as the + * given array has elements. References to the cluster nodes are stored + * in the given array. The initial state of the cluster consists of just + * an empty root node and a shared journal with only a single root + * revision. The caller of this method should have exclusive access to + * the created cluster. The caller is also responsible for calling + * {@link #tearDownCluster(MicroKernel[])} when the test cluster is + * no longer needed. + * + * @param cluster array to which references to all nodes of the + * created cluster should be stored + */ + void setUpCluster(MicroKernel[] cluster); + + /** + * Ensures that all content changes seen by one of the given cluster + * nodes are seen also by all the other given nodes. Used to help + * testing features like eventual consistency where the standard + * {@link MicroKernel} API doesn't make strong enough guarantees to + * enable writing a test case without a potentially unbounded wait for + * changes to propagate across the cluster. + * + * @param nodes cluster nodes to be synchronized + */ + void syncMicroKernelCluster(MicroKernel... nodes); + + /** + * Releases resources associated with the given {@link MicroKernel} + * cluster. The caller of {@link #setUpCluster(MicroKernel[])} shall + * call this method once the cluster is no longer needed. + * + * @param cluster array containing references to all nodes of the cluster + */ + void tearDownCluster(MicroKernel[] cluster); + +} diff --git a/oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/MicroKernelIT.java b/oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/MicroKernelIT.java new file mode 100644 index 00000000000..303fa0a8279 --- /dev/null +++ b/oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/MicroKernelIT.java @@ -0,0 +1,1291 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.test; + +import org.apache.jackrabbit.mk.api.MicroKernelException; +import org.apache.jackrabbit.mk.test.util.TestInputStream; +import org.apache.jackrabbit.mk.util.MicroKernelInputStream; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; + +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.junit.Assert.fail; + +/** + * Integration tests for verifying that a {@code MicroKernel} implementation + * obeys the contract of the {@code MicroKernel} API. + */ +@RunWith(Parameterized.class) +public class MicroKernelIT extends AbstractMicroKernelIT { + + public MicroKernelIT(MicroKernelFixture fixture) { + super(fixture, 1); + } + + @Override + protected void addInitialTestContent() { + mk.commit("/", "+\"test\" : {" + + "\"stringProp\":\"stringVal\"," + + "\"intProp\":42," + + "\"floatProp\":42.2," + + "\"booleanProp\": true," + + "\"multiIntProp\":[1,2,3]}", null, ""); + } + + @Test + public void revisionOps() { + String head = mk.getHeadRevision(); + assertNotNull(head); + + try { + Thread.sleep(100); + } catch (InterruptedException ignore) { + } + + long now = System.currentTimeMillis(); + + // get history since 'now' + JSONArray array = parseJSONArray(mk.getRevisionHistory(now, -1, null)); + // history should be empty since there was no commit since 'now' + assertEquals(0, array.size()); + + // get oldest available revision + array = parseJSONArray(mk.getRevisionHistory(0, 1, null)); + // there should be exactly 1 revision + assertEquals(1, array.size()); + + long ts0 = System.currentTimeMillis(); + + final int NUM_COMMITS = 100; + + // perform NUM_COMMITS commits + for (int i = 0; i < NUM_COMMITS; i++) { + mk.commit("/test", "+\"child" + i + "\":{}", null, "commit#" + i); + } + + // get oldest available revision + array = parseJSONArray(mk.getRevisionHistory(ts0, -1, null)); + // there should be exactly NUM_COMMITS revisions + assertEquals(NUM_COMMITS, array.size()); + long previousTS = ts0; + for (int i = 0; i < NUM_COMMITS; i++) { + JSONObject rev = getObjectArrayEntry(array, i); + assertPropertyExists(rev, "id", String.class); + assertPropertyExists(rev, "ts", Long.class); + // verify commit msg + assertPropertyValue(rev, "msg", "commit#" + i); + // verify chronological order + long ts = (Long) resolveValue(rev, "ts"); + assertTrue(previousTS <= ts); + previousTS = ts; + } + + // last revision should be the current head revision + assertPropertyValue(getObjectArrayEntry(array, array.size() - 1), "id", mk.getHeadRevision()); + + String fromRev = (String) resolveValue(getObjectArrayEntry(array, 0), "id"); + String toRev = (String) resolveValue(getObjectArrayEntry(array, array.size() - 1), "id"); + + // verify journal + array = parseJSONArray(mk.getJournal(fromRev, toRev, "")); + // there should be exactly NUM_COMMITS entries + assertEquals(NUM_COMMITS, array.size()); + // verify that 1st and last rev match fromRev and toRev + assertPropertyValue(getObjectArrayEntry(array, 0), "id", fromRev); + assertPropertyValue(getObjectArrayEntry(array, array.size() - 1), "id", toRev); + + previousTS = ts0; + for (int i = 0; i < NUM_COMMITS; i++) { + JSONObject rev = getObjectArrayEntry(array, i); + assertPropertyExists(rev, "id", String.class); + assertPropertyExists(rev, "ts", Long.class); + assertPropertyExists(rev, "changes", String.class); + // TODO verify json diff + // verify commit msg + assertPropertyValue(rev, "msg", "commit#" + i); + // verify chronological order + long ts = (Long) resolveValue(rev, "ts"); + assertTrue(previousTS <= ts); + previousTS = ts; + } + + // test with 'negative' range (from and to swapped) + array = parseJSONArray(mk.getJournal(toRev, fromRev, "")); + // there should be exactly 0 entries + assertEquals(0, array.size()); + } + + @Test + public void revisionOpsFiltered() { + // test getRevisionHistory/getJournal path filter + mk.commit("/test", "+\"foo\":{} +\"bar\":{}", null, ""); + + try { + Thread.sleep(100); + } catch (InterruptedException ignore) { + } + + long ts = System.currentTimeMillis(); + + String revFoo = mk.commit("/test/foo", "^\"p1\":123", null, ""); + String revBar = mk.commit("/test/bar", "^\"p2\":456", null, ""); + String rev0 = revFoo; + + // get history since ts (no filter) + JSONArray array = parseJSONArray(mk.getRevisionHistory(ts, -1, null)); + // history should contain 2 commits: revFoo and revBar + assertEquals(2, array.size()); + assertPropertyValue(getObjectArrayEntry(array, 0), "id", revFoo); + assertPropertyValue(getObjectArrayEntry(array, 1), "id", revBar); + + // get history since ts (non-matching filter) + array = parseJSONArray(mk.getRevisionHistory(ts, -1, "/blah")); + // history should contain 0 commits since filter doesn't match + assertEquals(0, array.size()); + + // get history since ts (filter on /test/bar) + array = parseJSONArray(mk.getRevisionHistory(ts, -1, "/test/bar")); + // history should contain 1 commit: revBar + assertEquals(1, array.size()); + assertPropertyValue(getObjectArrayEntry(array, 0), "id", revBar); + + // get journal (no filter) + array = parseJSONArray(mk.getJournal(rev0, null, "")); + // journal should contain 2 commits: revFoo and revBar + assertEquals(2, array.size()); + assertPropertyValue(getObjectArrayEntry(array, 0), "id", revFoo); + assertPropertyValue(getObjectArrayEntry(array, 1), "id", revBar); + + // get journal (non-matching filter) + array = parseJSONArray(mk.getJournal(rev0, null, "/blah")); + // journal should contain 0 commits since filter doesn't match + assertEquals(0, array.size()); + + // get journal (filter on /test/bar) + array = parseJSONArray(mk.getJournal(rev0, null, "/test/bar")); + // journal should contain 1 commit: revBar + assertEquals(1, array.size()); + assertPropertyValue(getObjectArrayEntry(array, 0), "id", revBar); + String diff = (String) resolveValue(getObjectArrayEntry(array, 0), "changes"); + assertNotNull(diff); + assertTrue(diff.contains("456")); + assertFalse(diff.contains("123")); + } + + @Test + public void diff() { + String rev0 = mk.getHeadRevision(); + + String rev1 = mk.commit("/test", "+\"target\":{}", null, null); + assertTrue(mk.nodeExists("/test/target", null)); + + // get reverse diff + String reverseDiff = mk.diff(rev1, rev0, null, -1); + assertNotNull(reverseDiff); + assertTrue(reverseDiff.length() > 0); + + // commit reverse diff + String rev2 = mk.commit("", reverseDiff, null, null); + assertFalse(mk.nodeExists("/test/target", null)); + + // diff of rev0->rev2 should be empty + assertEquals("", mk.diff(rev0, rev2, null, -1)); + } + + @Test + public void diffFiltered() { + String rev0 = mk.commit("/test", "+\"foo\":{} +\"bar\":{}", null, ""); + + String rev1 = mk.commit("/test/foo", "^\"p1\":123", null, ""); + String rev2 = mk.commit("/test/bar", "^\"p2\":456", null, ""); + + // test with path filter + String diff = mk.diff(rev0, rev2, "/test", -1); + assertNotNull(diff); + assertFalse(diff.isEmpty()); + assertTrue(diff.contains("foo")); + assertTrue(diff.contains("bar")); + assertTrue(diff.contains("123")); + assertTrue(diff.contains("456")); + + diff = mk.diff(rev0, rev2, "/test/foo", -1); + assertNotNull(diff); + assertFalse(diff.isEmpty()); + assertTrue(diff.contains("foo")); + assertFalse(diff.contains("bar")); + assertTrue(diff.contains("123")); + assertFalse(diff.contains("456")); + + // non-matching filter + diff = mk.diff(rev0, rev2, "/blah", -1); + assertNotNull(diff); + assertTrue(diff.isEmpty()); + } + + @Test + public void diffDepthLimited() { + // initial content (/test/foo) + String rev0 = mk.commit("/test", "+\"foo\":{}", null, ""); + + // add /test/foo/bar + String rev1 = mk.commit("/test/foo", "+\"bar\":{\"p1\":123}", null, ""); + + // modify property /test/foo/bar/p1 + String rev2 = mk.commit("/test/foo/bar", "^\"p1\":456", null, ""); + + // diff with depth -1 + assertEquals(mk.diff(rev0, rev2, "/", Integer.MAX_VALUE), mk.diff(rev0, rev2, "/", -1)); + + // diff with depth 5 + String diff = mk.diff(rev0, rev2, "/", 5); + // returned +"/test/foo/bar":{"p1":456} + assertNotNull(diff); + assertFalse(diff.isEmpty()); + assertTrue(diff.contains("/test/foo/bar")); + assertTrue(diff.contains("456")); + + // diff with depth 0 + diff = mk.diff(rev0, rev2, "/", 0); + // returned ^"/test", indicating that there are changes below /test + assertNotNull(diff); + assertFalse(diff.isEmpty()); + assertFalse(diff.contains("/test/foo")); + assertFalse(diff.contains("456")); + assertTrue(diff.contains("/test")); + assertTrue(diff.startsWith("^")); + + // diff with depth 1 + diff = mk.diff(rev0, rev2, "/", 1); + // returned ^"/test/foo", indicating that there are changes below /test/foo + assertNotNull(diff); + assertFalse(diff.isEmpty()); + assertFalse(diff.contains("/test/foo/bar")); + assertFalse(diff.contains("456")); + assertTrue(diff.contains("/test/foo")); + assertTrue(diff.startsWith("^")); + } + + @Test + public void snapshotIsolation() { + final int NUM_COMMITS = 100; + + String[] revs = new String[NUM_COMMITS]; + + // perform NUM_COMMITS commits + for (int i = 0; i < NUM_COMMITS; i++) { + revs[i] = mk.commit("/test", "^\"cnt\":" + i, null, null); + } + // verify that each revision contains the expected distinct property value + for (int i = 0; i < NUM_COMMITS; i++) { + JSONObject obj = parseJSONObject(mk.getNodes("/test", revs[i], 1, 0, -1, null)); + assertPropertyValue(obj, "cnt", (long) i); + } + } + + @Test + public void waitForCommit() throws InterruptedException { + final long SHORT_TIMEOUT = 1000; + final long LONG_TIMEOUT = 1000; + + // concurrent commit + String oldHead = mk.getHeadRevision(); + + Thread t = new Thread("") { + @Override + public void run() { + String newHead = mk.commit("/", "+\"foo\":{}", null, ""); + setName(newHead); + } + }; + t.start(); + String newHead = mk.waitForCommit(oldHead, LONG_TIMEOUT); + t.join(); + + assertFalse(oldHead.equals(newHead)); + assertEquals(newHead, t.getName()); + assertEquals(newHead, mk.getHeadRevision()); + + // the current head is already more recent than oldRevision; + // the method should return immediately (TIMEOUT not applied) + String currentHead = mk.getHeadRevision(); + long t0 = System.currentTimeMillis(); + newHead = mk.waitForCommit(oldHead, LONG_TIMEOUT); + long t1 = System.currentTimeMillis(); + assertTrue((t1 - t0) < LONG_TIMEOUT); + assertEquals(currentHead, newHead); + + // there's no more recent head available; + // the method should wait for the given short timeout + currentHead = mk.getHeadRevision(); + long t2 = System.currentTimeMillis(); + newHead = mk.waitForCommit(currentHead, SHORT_TIMEOUT); + long t3 = System.currentTimeMillis(); + assertTrue((t3 - t2) >= SHORT_TIMEOUT); + assertEquals(currentHead, newHead); + } + + @Test + public void addAndMove() { + String head = mk.getHeadRevision(); + head = mk.commit("", + "+\"/root\":{}\n" + + "+\"/root/a\":{}\n", + head, ""); + + head = mk.commit("", + "+\"/root/a/b\":{}\n" + + ">\"/root/a\":\"/root/c\"\n", + head, ""); + + assertFalse(mk.nodeExists("/root/a", head)); + assertTrue(mk.nodeExists("/root/c/b", head)); + } + + @Test + public void copy() { + mk.commit("/", "*\"test\":\"testCopy\"", null, ""); + + assertTrue(mk.nodeExists("/testCopy", null)); + assertTrue(mk.nodeExists("/test", null)); + + JSONObject obj = parseJSONObject(mk.getNodes("/test", null, 99, 0, -1, null)); + JSONObject obj1 = parseJSONObject(mk.getNodes("/testCopy", null, 99, 0, -1, null)); + assertEquals(obj, obj1); + } + + @Test + public void addAndCopy() { + mk.commit("/", + "+\"x\":{}\n" + + "+\"y\":{}\n", + null, ""); + + mk.commit("/", + "+\"x/a\":{}\n" + + "*\"x\":\"y/x1\"\n", + null, ""); + + assertTrue(mk.nodeExists("/x/a", null)); + assertTrue(mk.nodeExists("/y/x1/a", null)); + } + + @Test + public void copyToDescendant() { + mk.commit("/", + "+\"test/child\":{}\n" + + "*\"test\":\"test/copy\"\n", + null, ""); + + assertTrue(mk.nodeExists("/test/child", null)); + assertTrue(mk.nodeExists("/test/copy/child", null)); + JSONObject obj = parseJSONObject(mk.getNodes("/test", null, 99, 0, -1, null)); + assertPropertyValue(obj, ":childNodeCount", 2l); + assertPropertyValue(obj, "copy/:childNodeCount", 1l); + assertPropertyValue(obj, "copy/child/:childNodeCount", 0l); + + mk.commit("", "+\"/root\":{} +\"/root/N4\":{} *\"/root/N4\":\"/root/N4/N5\"", null, null); + assertTrue(mk.nodeExists("/root", null)); + assertTrue(mk.nodeExists("/root/N4", null)); + assertTrue(mk.nodeExists("/root/N4/N5", null)); + obj = parseJSONObject(mk.getNodes("/root", null, 99, 0, -1, null)); + assertPropertyValue(obj, ":childNodeCount", 1l); + assertPropertyValue(obj, "N4/:childNodeCount", 1l); + assertPropertyValue(obj, "N4/N5/:childNodeCount", 0l); + } + + @Test + public void getNodes() { + String head = mk.getHeadRevision(); + + // verify initial content + JSONObject obj = parseJSONObject(mk.getNodes("/", head, 1, 0, -1, null)); + assertPropertyValue(obj, "test/stringProp", "stringVal"); + assertPropertyValue(obj, "test/intProp", 42L); + assertPropertyValue(obj, "test/floatProp", 42.2); + assertPropertyValue(obj, "test/booleanProp", true); + assertPropertyValue(obj, "test/multiIntProp", new Object[]{1, 2, 3}); + } + + @Test + public void getNodesHash() { + // :hash must be explicitly specified in the filter + JSONObject obj = parseJSONObject(mk.getNodes("/", null, 1, 0, -1, null)); + assertPropertyNotExists(obj, ":hash"); + obj = parseJSONObject(mk.getNodes("/", null, 1, 0, -1, "{\"properties\":[\"*\"]}")); + assertPropertyNotExists(obj, ":hash"); + + // verify initial content with :hash property + obj = parseJSONObject(mk.getNodes("/", null, 1, 0, -1, "{\"properties\":[\"*\",\":hash\"]}")); + assertPropertyValue(obj, "test/booleanProp", true); + + if (obj.get(":hash") == null) { + // :hash is optional, an implementation might not support it + return; + } + + assertPropertyExists(obj, ":hash", String.class); + String hash0 = (String) resolveValue(obj, ":hash"); + + // modify a property and verify that the hash of the root node changed + mk.commit("/test", "^\"booleanProp\":false", null, null); + obj = parseJSONObject(mk.getNodes("/", null, 1, 0, -1, "{\"properties\":[\"*\",\":hash\"]}")); + assertPropertyValue(obj, "test/booleanProp", false); + + assertPropertyExists(obj, ":hash", String.class); + String hash1 = (String) resolveValue(obj, ":hash"); + + assertFalse(hash0.equals(hash1)); + + // undo property modification and verify that the hash + // of the root node is now the same as before the modification + mk.commit("/test", "^\"booleanProp\":true", null, null); + obj = parseJSONObject(mk.getNodes("/", null, 1, 0, -1, "{\"properties\":[\"*\",\":hash\"]}")); + assertPropertyValue(obj, "test/booleanProp", true); + + assertPropertyExists(obj, ":hash", String.class); + String hash2 = (String) resolveValue(obj, ":hash"); + + assertFalse(hash1.equals(hash2)); + assertTrue(hash0.equals(hash2)); + } + + @Test + public void getNodesChildNodeCount() { + // :childNodeCount is included per default + JSONObject obj = parseJSONObject(mk.getNodes("/", null, 1, 0, -1, null)); + assertPropertyExists(obj, ":childNodeCount", Long.class); + assertPropertyExists(obj, "test/:childNodeCount", Long.class); + long count = (Long) resolveValue(obj, ":childNodeCount") ; + assertEquals(count, mk.getChildNodeCount("/", null)); + + obj = parseJSONObject(mk.getNodes("/", null, 1, 0, -1, "{\"properties\":[\"*\"]}")); + assertPropertyExists(obj, ":childNodeCount", Long.class); + assertPropertyExists(obj, "test/:childNodeCount", Long.class); + count = (Long) resolveValue(obj, ":childNodeCount") ; + assertEquals(count, mk.getChildNodeCount("/", null)); + + // explicitly exclude :childNodeCount + obj = parseJSONObject(mk.getNodes("/", null, 1, 0, -1, "{\"properties\":[\"*\",\"-:childNodeCount\"]}")); + assertPropertyNotExists(obj, ":childNodeCount"); + assertPropertyNotExists(obj, "test/:childNodeCount"); + } + + @Test + public void getNodesFiltered() { + String head = mk.getHeadRevision(); + + // verify initial content using filter + String filter = "{ \"properties\" : [ \"*ntProp\", \"-mult*\" ] } "; + JSONObject obj = parseJSONObject(mk.getNodes("/", head, 1, 0, -1, filter)); + assertPropertyExists(obj, "test/intProp"); + assertPropertyNotExists(obj, "test/multiIntProp"); + assertPropertyNotExists(obj, "test/stringProp"); + assertPropertyNotExists(obj, "test/floatProp"); + assertPropertyNotExists(obj, "test/booleanProp"); + } + + @Test + public void getNodesNonExistingPath() { + String head = mk.getHeadRevision(); + + String nonExistingPath = "/test/" + System.currentTimeMillis(); + assertFalse(mk.nodeExists(nonExistingPath, head)); + + assertNull(mk.getNodes(nonExistingPath, head, 0, 0, -1, null)); + } + + @Test + public void getNodesNonExistingRevision() { + String nonExistingRev = "12345678"; + + try { + mk.nodeExists("/test", nonExistingRev); + fail("Success with non-existing revision: " + nonExistingRev); + } catch (MicroKernelException e) { + // expected + } + + try { + mk.getNodes("/test", nonExistingRev, 0, 0, -1, null); + fail("Success with non-existing revision: " + nonExistingRev); + } catch (MicroKernelException e) { + // expected + } + } + + @Test + public void getNodesDepth() { + mk.commit("", "+\"/testRoot\":{\"depth\":0}", null, ""); + mk.commit("/testRoot", "+\"a\":{\"depth\":1}\n" + + "+\"a/b\":{\"depth\":2}\n" + + "+\"a/b/c\":{\"depth\":3}\n" + + "+\"a/b/c/d\":{\"depth\":4}\n" + + "+\"a/b/c/d/e\":{\"depth\":5}\n", + null, ""); + + // depth = 0: properties, including :childNodeCount and empty child node objects + JSONObject obj = parseJSONObject(mk.getNodes("/testRoot", null, 0, 0, -1, null)); + assertPropertyValue(obj, "depth", 0l); + assertPropertyValue(obj, ":childNodeCount", 1l); + JSONObject child = resolveObjectValue(obj, "a"); + assertNotNull(child); + assertEquals(0, child.size()); + + // depth = 1: properties, child nodes, their properties (including :childNodeCount) + // and their empty child node objects + obj = parseJSONObject(mk.getNodes("/testRoot", null, 1, 0, -1, null)); + assertPropertyValue(obj, "depth", 0l); + assertPropertyValue(obj, ":childNodeCount", 1l); + assertPropertyValue(obj, "a/depth", 1l); + assertPropertyValue(obj, "a/:childNodeCount", 1l); + child = resolveObjectValue(obj, "a/b"); + assertNotNull(child); + assertEquals(0, child.size()); + + // depth = 2: [and so on...] + obj = parseJSONObject(mk.getNodes("/testRoot", null, 2, 0, -1, null)); + assertPropertyValue(obj, "depth", 0l); + assertPropertyValue(obj, ":childNodeCount", 1l); + assertPropertyValue(obj, "a/depth", 1l); + assertPropertyValue(obj, "a/:childNodeCount", 1l); + assertPropertyValue(obj, "a/b/depth", 2l); + assertPropertyValue(obj, "a/b/:childNodeCount", 1l); + child = resolveObjectValue(obj, "a/b/c"); + assertNotNull(child); + assertEquals(0, child.size()); + + // depth = 3: [and so on...] + obj = parseJSONObject(mk.getNodes("/testRoot", null, 3, 0, -1, null)); + assertPropertyValue(obj, "depth", 0l); + assertPropertyValue(obj, ":childNodeCount", 1l); + assertPropertyValue(obj, "a/depth", 1l); + assertPropertyValue(obj, "a/:childNodeCount", 1l); + assertPropertyValue(obj, "a/b/depth", 2l); + assertPropertyValue(obj, "a/b/:childNodeCount", 1l); + assertPropertyValue(obj, "a/b/c/depth", 3l); + assertPropertyValue(obj, "a/b/c/:childNodeCount", 1l); + child = resolveObjectValue(obj, "a/b/c/d"); + assertNotNull(child); + assertEquals(0, child.size()); + + // getNodes(path, revId) must return same result as getNodes(path, revId, 1, 0, -1, null) + obj = parseJSONObject(mk.getNodes("/testRoot", null, 1, 0, -1, null)); + JSONObject obj1 = parseJSONObject(mk.getNodes("/testRoot", null, 1, 0, -1, null)); + assertEquals(obj, obj1); + } + + @Test + public void getNodesOffset() { + // number of siblings (multiple of 10) + final int NUM_SIBLINGS = 1000; + // set of all sibling names + final Set siblingNames = new HashSet(NUM_SIBLINGS); + + // populate siblings + Random rand = new Random(); + StringBuffer sb = new StringBuffer("+\"/testRoot\":{"); + for (int i = 0; i < NUM_SIBLINGS; i++) { + String name = "n" + rand.nextLong(); + siblingNames.add(name); + sb.append("\n\""); + sb.append(name); + sb.append("\":{}"); + if (i < NUM_SIBLINGS - 1) { + sb.append(','); + } + } + sb.append("\n}"); + String head = mk.commit("", sb.toString(), null, ""); + + // get all siblings in one call + JSONObject obj = parseJSONObject(mk.getNodes("/testRoot", head, 0, 0, -1, null)); + assertPropertyValue(obj, ":childNodeCount", (long) NUM_SIBLINGS); + assertEquals((long) NUM_SIBLINGS, mk.getChildNodeCount("/testRoot", head)); + assertEquals(siblingNames, getNodeNames(obj)); + + // list of sibling names in iteration order + final List orderedSiblingNames = new ArrayList(NUM_SIBLINGS); + + // get siblings one by one + for (int i = 0; i < NUM_SIBLINGS; i++) { + obj = parseJSONObject(mk.getNodes("/testRoot", head, 0, i, 1, null)); + assertPropertyValue(obj, ":childNodeCount", (long) NUM_SIBLINGS); + Set set = getNodeNames(obj); + assertEquals(1, set.size()); + orderedSiblingNames.add(set.iterator().next()); + } + + // check completeness + Set names = new HashSet(siblingNames); + names.removeAll(orderedSiblingNames); + assertTrue(names.isEmpty()); + + // we've now established the expected iteration order + + // get siblings in 10 chunks + for (int i = 0; i < 10; i++) { + obj = parseJSONObject(mk.getNodes("/testRoot", head, 0, i * 10, NUM_SIBLINGS / 10, null)); + assertPropertyValue(obj, ":childNodeCount", (long) NUM_SIBLINGS); + names = getNodeNames(obj); + assertEquals(NUM_SIBLINGS / 10, names.size()); + List subList = orderedSiblingNames.subList(i * 10, (i * 10) + (NUM_SIBLINGS / 10)); + names.removeAll(subList); + assertTrue(names.isEmpty()); + } + + // test offset with filter + try { + parseJSONObject(mk.getNodes("/testRoot", head, 0, 10, NUM_SIBLINGS / 10, "{\"nodes\":[\"n0*\"]}")); + fail(); + } catch (Throwable e) { + // expected + } + } + + @Test + public void getNodesMaxNodeCount() { + // number of siblings + final int NUM_SIBLINGS = 100; + + // populate siblings + StringBuffer sb = new StringBuffer("+\"/testRoot\":{"); + for (int i = 0; i < NUM_SIBLINGS; i++) { + String name = "n" + i; + sb.append("\n\""); + sb.append(name); + sb.append("\":{"); + + for (int n = 0; n < NUM_SIBLINGS; n++) { + String childName = "n" + n; + sb.append("\n\""); + sb.append(childName); + sb.append("\":{}"); + if (n < NUM_SIBLINGS - 1) { + sb.append(','); + } + } + sb.append("}"); + if (i < NUM_SIBLINGS - 1) { + sb.append(','); + } + } + sb.append("\n}"); + String head = mk.commit("", sb.toString(), null, ""); + + // get all siblings + JSONObject obj = parseJSONObject(mk.getNodes("/testRoot", head, 1, 0, -1, null)); + assertPropertyValue(obj, ":childNodeCount", (long) NUM_SIBLINGS); + assertEquals((long) NUM_SIBLINGS, mk.getChildNodeCount("/testRoot", head)); + Set names = getNodeNames(obj); + assertTrue(names.size() == NUM_SIBLINGS); + String childName = names.iterator().next(); + JSONObject childObj = resolveObjectValue(obj, childName); + assertPropertyValue(childObj, ":childNodeCount", (long) NUM_SIBLINGS); + assertTrue(getNodeNames(childObj).size() == NUM_SIBLINGS); + + // get max 10 siblings + int maxSiblings = 10; + obj = parseJSONObject(mk.getNodes("/testRoot", head, 1, 0, maxSiblings, null)); + assertPropertyValue(obj, ":childNodeCount", (long) NUM_SIBLINGS); + assertEquals((long) NUM_SIBLINGS, mk.getChildNodeCount("/testRoot", head)); + names = getNodeNames(obj); + assertEquals(maxSiblings, names.size()); + childName = names.iterator().next(); + childObj = resolveObjectValue(obj, childName); + assertPropertyValue(childObj, ":childNodeCount", (long) NUM_SIBLINGS); + assertTrue(getNodeNames(childObj).size() == maxSiblings); + + // get max 5 siblings using filter + maxSiblings = 5; + obj = parseJSONObject(mk.getNodes("/testRoot", head, 1, 0, maxSiblings, "{\"nodes\":[\"n1*\"]}")); + assertPropertyValue(obj, ":childNodeCount", (long) NUM_SIBLINGS); + assertEquals((long) NUM_SIBLINGS, mk.getChildNodeCount("/testRoot", head)); + names = getNodeNames(obj); + assertTrue(names.size() == maxSiblings); + childName = names.iterator().next(); + childObj = resolveObjectValue(obj, childName); + assertPropertyValue(childObj, ":childNodeCount", (long) NUM_SIBLINGS); + assertTrue(getNodeNames(childObj).size() == maxSiblings); + } + + @Test + public void getNodesRevision() { + // 1st pass + long tst = System.currentTimeMillis(); + String head = mk.commit("/test", "^\"tst\":" + tst, null, null); + assertEquals(head, mk.getHeadRevision()); + // test getNodes with 'null' revision + assertPropertyValue(parseJSONObject(mk.getNodes("/test", null, 1, 0, -1, null)), "tst", tst); + // test getNodes with specific revision + assertPropertyValue(parseJSONObject(mk.getNodes("/test", head, 1, 0, -1, null)), "tst", tst); + + // 2nd pass + ++tst; + String oldHead = head; + head = mk.commit("/test", "^\"tst\":" + tst, null, null); + assertFalse(head.equals(oldHead)); + assertEquals(head, mk.getHeadRevision()); + // test getNodes with 'null' revision + assertPropertyValue(parseJSONObject(mk.getNodes("/test", null, 1, 0, -1, null)), "tst", tst); + // test getNodes with specific revision + assertPropertyValue(parseJSONObject(mk.getNodes("/test", head, 1, 0, -1, null)), "tst", tst); + } + + @Test + public void missingName() { + String head = mk.getHeadRevision(); + + assertTrue(mk.nodeExists("/test", head)); + try { + String path = "/test/"; + mk.getNodes(path, head, 1, 0, -1, null); + fail("Success with invalid path: " + path); + } catch (AssertionError e) { + // expected + } catch (MicroKernelException e) { + // expected + } + } + + @Test + public void addNodeWithRelativePath() { + String head = mk.getHeadRevision(); + + head = mk.commit("/", "+\"foo\" : {} \n+\"foo/bar\" : {}", head, ""); + assertTrue(mk.nodeExists("/foo", head)); + assertTrue(mk.nodeExists("/foo/bar", head)); + } + + @Test + public void commitWithEmptyPath() { + String head = mk.getHeadRevision(); + + head = mk.commit("", "+\"/ene\" : {}\n+\"/ene/mene\" : {}\n+\"/ene/mene/muh\" : {}", head, ""); + assertTrue(mk.nodeExists("/ene/mene/muh", head)); + } + + @Test + public void addPropertyWithRelativePath() { + String head = mk.getHeadRevision(); + + head = mk.commit("/", + "+\"fuu\" : {} \n" + + "^\"fuu/bar\" : 42", head, ""); + JSONObject obj = parseJSONObject(mk.getNodes("/fuu", head, 1, 0, -1, null)); + assertPropertyValue(obj, "bar", 42L); + } + + @Test + public void addMultipleNodes() { + String head = mk.getHeadRevision(); + + long millis = System.currentTimeMillis(); + String node1 = "n1_" + millis; + String node2 = "n2_" + millis; + head = mk.commit("/", "+\"" + node1 + "\" : {} \n+\"" + node2 + "\" : {}\n", head, ""); + assertTrue(mk.nodeExists('/' + node1, head)); + assertTrue(mk.nodeExists('/' + node2, head)); + } + + @Test + public void addDeepNodes() { + String head = mk.getHeadRevision(); + + head = mk.commit("/", + "+\"a\" : {} \n" + + "+\"a/b\" : {} \n" + + "+\"a/b/c\" : {} \n" + + "+\"a/b/c/d\" : {} \n", + head, ""); + + assertTrue(mk.nodeExists("/a", head)); + assertTrue(mk.nodeExists("/a/b", head)); + assertTrue(mk.nodeExists("/a/b/c", head)); + assertTrue(mk.nodeExists("/a/b/c/d", head)); + } + + @Test + public void addItemsIncrementally() { + String head = mk.getHeadRevision(); + + String node = "n_" + System.currentTimeMillis(); + + head = mk.commit("/", + "+\"" + node + "\" : {} \n" + + "+\"" + node + "/child1\" : {} \n" + + "+\"" + node + "/child2\" : {} \n" + + "+\"" + node + "/child1/grandchild11\" : {} \n" + + "^\"" + node + "/prop1\" : 41\n" + + "^\"" + node + "/child1/prop2\" : 42\n" + + "^\"" + node + "/child1/grandchild11/prop3\" : 43", + head, ""); + + JSONObject obj = parseJSONObject(mk.getNodes('/' + node, head, 3, 0, -1, null)); + assertPropertyValue(obj, "prop1", 41L); + assertPropertyValue(obj, ":childNodeCount", 2L); + assertPropertyValue(obj, "child1/prop2", 42L); + assertPropertyValue(obj, "child1/:childNodeCount", 1L); + assertPropertyValue(obj, "child1/grandchild11/prop3", 43L); + assertPropertyValue(obj, "child1/grandchild11/:childNodeCount", 0L); + assertPropertyValue(obj, "child2/:childNodeCount", 0L); + } + + @Test + public void removeNode() { + String head = mk.getHeadRevision(); + String node = "removeNode_" + System.currentTimeMillis(); + + head = mk.commit("/", "+\"" + node + "\" : {\"child\":{}}", head, ""); + + head = mk.commit('/' + node, "-\"child\"", head, ""); + JSONObject obj = parseJSONObject(mk.getNodes('/' + node, head, 1, 0, -1, null)); + assertPropertyValue(obj, ":childNodeCount", 0L); + } + + @Test + public void moveNode() { + String rev0 = mk.getHeadRevision(); + JSONObject obj = parseJSONObject(mk.getNodes("/test", null, 99, 0, -1, null)); + mk.commit("/", ">\"test\" : \"moved\"", null, ""); + assertTrue(mk.nodeExists("/test", rev0)); + assertFalse(mk.nodeExists("/test", null)); + assertTrue(mk.nodeExists("/moved", null)); + JSONObject obj1 = parseJSONObject(mk.getNodes("/moved", null, 99, 0, -1, null)); + assertEquals(obj, obj1); + } + + @Test + public void illegalMove() { + mk.commit("", "+\"/test/sub\":{}", null, ""); + try { + // try to move /test to /test/sub/test + mk.commit("/", "> \"test\": \"/test/sub/test\"", null, ""); + fail(); + } catch (Exception e) { + // expected + } + } + + @Test + public void overwritingMove() { + String head = mk.getHeadRevision(); + + head = mk.commit("/", "+\"a\" : {} \n+\"b\" : {} \n", head, ""); + try { + mk.commit("/", ">\"a\" : \"b\" ", head, ""); + fail(); + } catch (MicroKernelException e) { + // expected + } + } + + @Test + public void conflictingMove() { + String head = mk.getHeadRevision(); + + head = mk.commit("/", "+\"a\" : {} \n+\"b\" : {}\n", head, ""); + + String r1 = mk.commit("/", ">\"a\" : \"b/a\"", head, ""); + assertFalse(mk.nodeExists("/a", r1)); + assertTrue(mk.nodeExists("/b", r1)); + assertTrue(mk.nodeExists("/b/a", r1)); + + try { + mk.commit("/", ">\"b\" : \"a/b\"", head, ""); + fail(); + } catch (MicroKernelException e) { + // expected + } + } + + @Test + public void conflictingAddDelete() { + String head = mk.getHeadRevision(); + + head = mk.commit("/", "+\"a\" : {} \n+\"b\" : {}\n", head, ""); + + String r1 = mk.commit("/", "-\"b\" \n +\"a/x\" : {}", head, ""); + assertFalse(mk.nodeExists("/b", r1)); + assertTrue(mk.nodeExists("/a", r1)); + assertTrue(mk.nodeExists("/a/x", r1)); + + try { + mk.commit("/", "-\"a\" \n +\"b/x\" : {}", head, ""); + fail(); + } catch (MicroKernelException e) { + // expected + } + } + + @Test + public void removeProperty() { + String head = mk.getHeadRevision(); + long t = System.currentTimeMillis(); + String node = "removeProperty_" + t; + + head = mk.commit("/", "+\"" + node + "\" : {\"prop\":\"value\"}", head, ""); + + head = mk.commit("/", "^\"" + node + "/prop\" : null", head, ""); + JSONObject obj = parseJSONObject(mk.getNodes('/' + node, head, 1, 0, -1, null)); + assertPropertyValue(obj, ":childNodeCount", 0L); + } + + @Test + public void branchAndMerge() { + // make sure /branch doesn't exist in head + assertFalse(mk.nodeExists("/branch", null)); + + // create a branch on head + String branchRev = mk.branch(null); + String branchRootRev = branchRev; + + // add a node /branch in branchRev + branchRev = mk.commit("", "+\"/branch\":{}", branchRev, ""); + // make sure /branch doesn't exist in head + assertFalse(mk.nodeExists("/branch", null)); + // make sure /branch does exist in branchRev + assertTrue(mk.nodeExists("/branch", branchRev)); + + // add a node /branch/foo in branchRev + branchRev = mk.commit("", "+\"/branch/foo\":{}", branchRev, ""); + + // make sure branchRev doesn't show up in revision history + String hist = mk.getRevisionHistory(0, -1, null); + JSONArray array = parseJSONArray(hist); + for (Object entry : array) { + assertTrue(entry instanceof JSONObject); + JSONObject rev = (JSONObject) entry; + assertFalse(branchRev.equals(rev.get("id"))); + } + + // add a node /test123 in head + mk.commit("", "+\"/test123\":{}", null, ""); + // make sure /test123 doesn't exist in branchRev + assertFalse(mk.nodeExists("/test123", branchRev)); + + // merge branchRev with head + String newHead = mk.merge(branchRev, ""); + // make sure /test123 still exists in head + assertTrue(mk.nodeExists("/test123", null)); + // make sure /branch/foo does now exist in head + assertTrue(mk.nodeExists("/branch/foo", null)); + + try { + mk.getJournal(branchRootRev, null, "/"); + fail("getJournal should throw for branch revisions"); + } catch (MicroKernelException e) { + // expected + } + try { + mk.getJournal(branchRootRev, branchRev, "/"); + fail("getJournal should throw for branch revisions"); + } catch (MicroKernelException e) { + // expected + } + + String jrnl = mk.getJournal(newHead, newHead, "/"); + array = parseJSONArray(jrnl); + assertEquals(1, array.size()); + JSONObject rev = getObjectArrayEntry(array, 0); + assertPropertyValue(rev, "id", newHead); + String diff = (String) resolveValue(rev, "changes"); + // TODO properly verify json diff format + // make sure json diff contains +"/branch":{...} + assertTrue(diff.matches("\\s*\\+\\s*\"/branch\"\\s*:\\s*\\{\\s*\"foo\"\\s*:\\s*\\{\\s*\\}\\s*\\}\\s*")); + } + + @Test + public void oneBranchAddedChildren1() { + addNodes(null, "/trunk", "/trunk/child1"); + assertNodesExist(null, "/trunk", "/trunk/child1"); + + String branchRev = mk.branch(null); + + branchRev = addNodes(branchRev, "/branch1", "/branch1/child1"); + assertNodesExist(branchRev, "/trunk", "/trunk/child1"); + assertNodesExist(branchRev, "/branch1", "/branch1/child1"); + assertNodesNotExist(null, "/branch1", "/branch1/child1"); + + addNodes(null, "/trunk/child2"); + assertNodesExist(null, "/trunk/child2"); + assertNodesNotExist(branchRev, "/trunk/child2"); + + mk.merge(branchRev, ""); + assertNodesExist(null, "/trunk", "/trunk/child1", "/trunk/child2", "/branch1", "/branch1/child1"); + } + + @Test + public void oneBranchAddedChildren2() { + addNodes(null, "/trunk", "/trunk/child1"); + assertNodesExist(null, "/trunk", "/trunk/child1"); + + String branchRev = mk.branch(null); + + branchRev = addNodes(branchRev, "/trunk/child1/child2"); + assertNodesExist(branchRev, "/trunk", "/trunk/child1"); + assertNodesExist(branchRev, "/trunk/child1/child2"); + assertNodesNotExist(null, "/trunk/child1/child2"); + + addNodes(null, "/trunk/child3"); + assertNodesExist(null, "/trunk/child3"); + assertNodesNotExist(branchRev, "/trunk/child3"); + + mk.merge(branchRev, ""); + assertNodesExist(null, "/trunk", "/trunk/child1", "/trunk/child1/child2", "/trunk/child3"); + } + + @Test + public void oneBranchAddedChildren3() { + addNodes(null, "/root", "/root/child1"); + assertNodesExist(null, "/root", "/root/child1"); + + String branchRev = mk.branch(null); + + addNodes(null, "/root/child2"); + assertNodesExist(null, "/root", "/root/child1", "/root/child2"); + assertNodesExist(branchRev, "/root", "/root/child1"); + assertNodesNotExist(branchRev, "/root/child2"); + + branchRev = addNodes(branchRev, "/root/child1/child3", "/root/child4"); + assertNodesExist(branchRev, "/root", "/root/child1", "/root/child1/child3", "/root/child4"); + assertNodesNotExist(branchRev, "/root/child2"); + assertNodesExist(null, "/root", "/root/child1", "/root/child2"); + assertNodesNotExist(null, "/root/child1/child3", "/root/child4"); + + mk.merge(branchRev, ""); + assertNodesExist(null, "/root", "/root/child1", "/root/child2", + "/root/child1/child3", "/root/child4"); + } + + @Test + public void oneBranchRemovedChildren() { + addNodes(null, "/trunk", "/trunk/child1"); + assertNodesExist(null, "/trunk", "/trunk/child1"); + + String branchRev = mk.branch(null); + + branchRev = removeNodes(branchRev, "/trunk/child1"); + assertNodesExist(branchRev, "/trunk"); + assertNodesNotExist(branchRev, "/trunk/child1"); + assertNodesExist(null, "/trunk", "/trunk/child1"); + } + + @Test + public void oneBranchChangedProperties() { + addNodes(null, "/trunk", "/trunk/child1"); + setProp(null, "/trunk/child1/prop1", "value1"); + setProp(null, "/trunk/child1/prop2", "value2"); + assertNodesExist(null, "/trunk", "/trunk/child1"); + assertPropExists(null, "/trunk/child1", "prop1"); + assertPropExists(null, "/trunk/child1", "prop2"); + + String branchRev = mk.branch(null); + + branchRev = setProp(branchRev, "/trunk/child1/prop1", "value1a"); + branchRev = setProp(branchRev, "/trunk/child1/prop2", null); + branchRev = setProp(branchRev, "/trunk/child1/prop3", "value3"); + assertPropValue(branchRev, "/trunk/child1", "prop1", "value1a"); + assertPropNotExists(branchRev, "/trunk/child1", "prop2"); + assertPropValue(branchRev, "/trunk/child1", "prop3", "value3"); + assertPropValue(null, "/trunk/child1", "prop1", "value1"); + assertPropExists(null, "/trunk/child1", "prop2"); + assertPropNotExists(null, "/trunk/child1", "prop3"); + + mk.merge(branchRev, ""); + assertPropValue(null, "/trunk/child1", "prop1", "value1a"); + assertPropNotExists(null, "/trunk/child1", "prop2"); + assertPropValue(null, "/trunk/child1", "prop3", "value3"); + } + + @Test + public void oneBranchAddedSubChildren() { + addNodes(null, "/trunk", "/trunk/child1", "/trunk/child1/child2", "/trunk/child1/child2/child3"); + assertNodesExist(null, "/trunk", "/trunk/child1", "/trunk/child1/child2", "/trunk/child1/child2/child3"); + + String branchRev = mk.branch(null); + + branchRev = addNodes(branchRev, "/branch1", "/branch1/child1", "/branch1/child1/child2", "/branch1/child1/child2/child3"); + assertNodesExist(branchRev, "/trunk", "/trunk/child1", "/trunk/child1/child2", "/trunk/child1/child2/child3"); + assertNodesExist(branchRev, "/branch1", "/branch1/child1", "/branch1/child1/child2", "/branch1/child1/child2/child3"); + assertNodesNotExist(null, "/branch1", "/branch1/child1", "/branch1/child1/child2", "/branch1/child1/child2/child3"); + + addNodes(null, "/trunk/child1/child2/child3/child4", "/trunk/child5"); + assertNodesExist(null, "/trunk/child1/child2/child3/child4", "/trunk/child5"); + assertNodesNotExist(branchRev, "/trunk/child1/child2/child3/child4", "/trunk/child5"); + + mk.merge(branchRev, ""); + assertNodesExist(null, "/trunk", "/trunk/child1", "/trunk/child1/child2", "/trunk/child1/child2/child3", "/trunk/child1/child2/child3/child4"); + assertNodesExist(null, "/branch1", "/branch1/child1", "/branch1/child1/child2", "/branch1/child1/child2/child3"); + } + + @Test + public void oneBranchAddedChildrenAndAddedProperties() { + addNodes(null, "/trunk", "/trunk/child1"); + setProp(null, "/trunk/child1/prop1", "value1"); + setProp(null, "/trunk/child1/prop2", "value2"); + assertNodesExist(null, "/trunk", "/trunk/child1"); + assertPropExists(null, "/trunk/child1", "prop1"); + assertPropExists(null, "/trunk/child1", "prop2"); + + String branchRev = mk.branch(null); + + branchRev = addNodes(branchRev, "/branch1", "/branch1/child1"); + branchRev = setProp(branchRev, "/branch1/child1/prop1", "value1"); + branchRev = setProp(branchRev, "/branch1/child1/prop2", "value2"); + assertNodesExist(branchRev, "/trunk", "/trunk/child1"); + assertPropExists(branchRev, "/trunk/child1", "prop1"); + assertPropExists(branchRev, "/trunk/child1", "prop2"); + assertNodesExist(branchRev, "/branch1", "/branch1/child1"); + assertPropExists(branchRev, "/branch1/child1", "prop1"); + assertPropExists(branchRev, "/branch1/child1", "prop2"); + assertNodesNotExist(null, "/branch1", "/branch1/child1"); + assertPropNotExists(null, "/branch1/child1", "prop1"); + assertPropNotExists(null, "/branch1/child1", "prop2"); + + mk.merge(branchRev, ""); + assertNodesExist(null, "/trunk", "/trunk/child1"); + assertPropExists(null, "/trunk/child1", "prop1"); + assertPropExists(null, "/trunk/child1", "prop2"); + assertNodesExist(null, "/branch1", "/branch1/child1"); + assertPropExists(null, "/branch1/child1", "prop1"); + assertPropExists(null, "/branch1/child1", "prop2"); + } + + @Test + public void twoBranchesAddedChildren1() { + addNodes(null, "/trunk", "/trunk/child1"); + assertNodesExist(null, "/trunk", "/trunk/child1"); + + String branchRev1 = mk.branch(null); + String branchRev2 = mk.branch(null); + + branchRev1 = addNodes(branchRev1, "/branch1", "/branch1/child1"); + branchRev2 = addNodes(branchRev2, "/branch2", "/branch2/child2"); + assertNodesExist(branchRev1, "/trunk", "/trunk/child1"); + assertNodesExist(branchRev2, "/trunk", "/trunk/child1"); + assertNodesExist(branchRev1, "/branch1/child1"); + assertNodesNotExist(branchRev1, "/branch2/child2"); + assertNodesExist(branchRev2, "/branch2/child2"); + assertNodesNotExist(branchRev2, "/branch1/child1"); + assertNodesNotExist(null, "/branch1/child1", "/branch2/child2"); + + addNodes(null, "/trunk/child2"); + assertNodesExist(null, "/trunk/child2"); + assertNodesNotExist(branchRev1, "/trunk/child2"); + assertNodesNotExist(branchRev2, "/trunk/child2"); + + mk.merge(branchRev1, ""); + assertNodesExist(null, "/trunk", "/branch1", "/branch1/child1"); + assertNodesNotExist(null, "/branch2", "/branch2/child2"); + + mk.merge(branchRev2, ""); + assertNodesExist(null, "/trunk", "/branch1", "/branch1/child1", "/branch2", "/branch2/child2"); + } + + @Test + public void emptyMergeCausesNoChange() { + String rev1 = mk.commit("", "+\"/child1\":{}", null, ""); + + String branchRev = mk.branch(null); + branchRev = mk.commit("", "+\"/child2\":{}", branchRev, ""); + branchRev = mk.commit("", "-\"/child2\"", branchRev, ""); + + String rev2 = mk.merge(branchRev, ""); + + assertTrue(mk.nodeExists("/child1", null)); + assertFalse(mk.nodeExists("/child2", null)); + assertEquals(rev1, rev2); + } + + @Test + public void trunkMergeNotAllowed() { + String rev = mk.commit("", "+\"/child1\":{}", null, ""); + try { + mk.merge(rev, ""); + fail("Exception expected"); + } catch (Exception expected) {} + } + + @Test + public void testSmallBlob() { + testBlobs(1234, 8 * 1024); + } + + @Test + public void testMediumBlob() { + testBlobs(1234567, 8 * 1024); + } + + @Test + public void testLargeBlob() { + testBlobs(32 * 1024 * 1024, 1024 * 1024); + } + + private void testBlobs(int size, int bufferSize) { + // write data + TestInputStream in = new TestInputStream(size); + String id = mk.write(in); + assertNotNull(id); + assertTrue(in.isClosed()); + + // write identical data + in = new TestInputStream(size); + String id1 = mk.write(in); + assertNotNull(id1); + assertTrue(in.isClosed()); + // both id's must be identical since they refer to identical data + assertEquals(id, id1); + + // verify length + assertEquals(mk.getLength(id), size); + + // verify data + InputStream in1 = new TestInputStream(size); + InputStream in2 = new BufferedInputStream( + new MicroKernelInputStream(mk, id), bufferSize); + try { + while (true) { + int x = in1.read(); + int y = in2.read(); + if (x == -1 || y == -1) { + if (x == y) { + break; + } + } + assertEquals("data does not match", x, y); + } + } catch (IOException e) { + fail(e.getMessage()); + } + } +} diff --git a/oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/MicroKernelTestSuite.java b/oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/MicroKernelTestSuite.java new file mode 100644 index 00000000000..bf6e3c72e53 --- /dev/null +++ b/oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/MicroKernelTestSuite.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.mk.test; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ + MicroKernelIT.class, + DataStoreIT.class +}) +public class MicroKernelTestSuite { +} diff --git a/oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/util/TestInputStream.java b/oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/util/TestInputStream.java new file mode 100644 index 00000000000..e848da0f292 --- /dev/null +++ b/oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/util/TestInputStream.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.test.util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Random; + +/** + * An {@code InputStream} based on pseudo-random data useful for testing. + *

    + * Instances created with identical parameter values do produce identical byte + * sequences. + */ +public class TestInputStream extends InputStream { + + private final Random random; + private final long length; + + private long pos; + private boolean closed; + + public TestInputStream(long length) { + this(0, length); + } + + public TestInputStream(long seed, long length) { + super(); + random = new Random(seed); + if (length < 0) { + throw new IllegalArgumentException("length cannot be negative"); + } + this.length = length; + pos = 0; + closed = false; + } + + public boolean isClosed() { + return closed; + } + + @Override + public long skip(long n) throws IOException { + if (n <= 0) { + return 0; + } + + long skipped; + for (skipped = 0; skipped < n; skipped++) { + if (read() == -1) { + break; + } + } + return skipped; + } + + @Override + public int available() throws IOException { + return (int) Math.min(Integer.MAX_VALUE, length - pos); + } + + @Override + public void close() throws IOException { + super.close(); + closed = true; + } + + @Override + public int read() throws IOException { + if (pos >= length) { + return -1; + } + pos++; + return random.nextInt() & 0xff; + } +} diff --git a/oak-it/mk/src/test/java/org/apache/jackrabbit/mk/test/ClientServerFixture.java b/oak-it/mk/src/test/java/org/apache/jackrabbit/mk/test/ClientServerFixture.java new file mode 100644 index 00000000000..32abe73a245 --- /dev/null +++ b/oak-it/mk/src/test/java/org/apache/jackrabbit/mk/test/ClientServerFixture.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.mk.test; + +import java.io.IOException; +import java.net.InetSocketAddress; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.client.Client; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +import org.apache.jackrabbit.mk.server.Server; +import org.apache.jackrabbit.mk.test.MicroKernelFixture; + +public class ClientServerFixture implements MicroKernelFixture { + + private MicroKernelImpl mk; + private Server server; + + @Override + public void setUpCluster(MicroKernel[] cluster) { + mk = new MicroKernelImpl(); + server = new Server(mk); + try { + server.start(); + } catch (IOException e) { + throw new IllegalArgumentException(e.getMessage()); + } + + InetSocketAddress address = server.getAddress(); + cluster[0] = new Client(address); + for (int i = 1; i < cluster.length; i++) { + cluster[i] = new Client(address); + } + } + + @Override + public void syncMicroKernelCluster(MicroKernel... nodes) { + } + + @Override + public void tearDownCluster(MicroKernel[] cluster) { + server.stop(); + mk.dispose(); + } +} diff --git a/oak-it/mk/src/test/java/org/apache/jackrabbit/mk/test/EverythingIT.java b/oak-it/mk/src/test/java/org/apache/jackrabbit/mk/test/EverythingIT.java new file mode 100644 index 00000000000..71d2832357c --- /dev/null +++ b/oak-it/mk/src/test/java/org/apache/jackrabbit/mk/test/EverythingIT.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.test; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ MicroKernelTestSuite.class }) +public class EverythingIT { +} diff --git a/oak-it/mk/src/test/java/org/apache/jackrabbit/mk/test/MicroKernelImplFixture.java b/oak-it/mk/src/test/java/org/apache/jackrabbit/mk/test/MicroKernelImplFixture.java new file mode 100644 index 00000000000..6fd58f0397a --- /dev/null +++ b/oak-it/mk/src/test/java/org/apache/jackrabbit/mk/test/MicroKernelImplFixture.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.mk.test; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; + +public class MicroKernelImplFixture implements MicroKernelFixture { + + @Override + public void setUpCluster(MicroKernel[] cluster) { + MicroKernel mk = new MicroKernelImpl(); + for (int i = 0; i < cluster.length; i++) { + cluster[i] = mk; + } + } + + @Override + public void syncMicroKernelCluster(MicroKernel... nodes) { + } + + @Override + public void tearDownCluster(MicroKernel[] cluster) { + for (int i = 0; i < cluster.length; i++) { + ((MicroKernelImpl) cluster[i]).dispose(); + } + } + +} diff --git a/oak-it/mk/src/test/resources/META-INF/services/org.apache.jackrabbit.mk.test.MicroKernelFixture b/oak-it/mk/src/test/resources/META-INF/services/org.apache.jackrabbit.mk.test.MicroKernelFixture new file mode 100644 index 00000000000..293393c51e6 --- /dev/null +++ b/oak-it/mk/src/test/resources/META-INF/services/org.apache.jackrabbit.mk.test.MicroKernelFixture @@ -0,0 +1,17 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +org.apache.jackrabbit.mk.test.MicroKernelImplFixture +org.apache.jackrabbit.mk.test.ClientServerFixture diff --git a/oak-it/mk/src/test/resources/logback-test.xml b/oak-it/mk/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..ef8db071c00 --- /dev/null +++ b/oak-it/mk/src/test/resources/logback-test.xml @@ -0,0 +1,31 @@ + + + + + + target/test.log + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + + + + diff --git a/oak-it/osgi/pom.xml b/oak-it/osgi/pom.xml new file mode 100644 index 00000000000..89f0f8e8152 --- /dev/null +++ b/oak-it/osgi/pom.xml @@ -0,0 +1,151 @@ + + + + + + 4.0.0 + + + org.apache.jackrabbit + oak-parent + 0.6-SNAPSHOT + ../../oak-parent/pom.xml + + + oak-it-osgi + Oak Integration Tests for OSGi deployments + + + true + 2.4.0 + + + + + + maven-assembly-plugin + + + pre-integration-test + + single + + + test-bundles.xml + test + false + + + + + + maven-failsafe-plugin + + + + WARN + + + + + + + + + + org.apache.jackrabbit + oak-commons + ${project.version} + test + + + org.apache.jackrabbit + oak-mk-api + ${project.version} + test + + + org.apache.jackrabbit + oak-mk + ${project.version} + test + + + org.apache.jackrabbit + oak-mk-remote + ${project.version} + test + + + org.apache.jackrabbit + oak-core + ${project.version} + test + + + org.apache.jackrabbit + oak-jcr + ${project.version} + test + + + + org.ops4j.pax.exam + pax-exam-junit4 + ${pax.exam.version} + test + + + org.apache.geronimo.specs + geronimo-atinject_1.0_spec + 1.0 + test + + + org.ops4j.pax.exam + pax-exam-container-native + ${pax.exam.version} + test + + + org.apache.felix + org.apache.felix.framework + 4.0.1 + test + + + org.ops4j.pax.exam + pax-exam-link-assembly + ${pax.exam.version} + test + + + org.ops4j.pax.url + pax-url-aether + 1.3.3 + test + + + org.slf4j + slf4j-simple + 1.6.1 + test + + + + diff --git a/oak-it/osgi/src/test/java/org/apache/jackrabbit/oak/osgi/OSGiIT.java b/oak-it/osgi/src/test/java/org/apache/jackrabbit/oak/osgi/OSGiIT.java new file mode 100644 index 00000000000..b624f63539f --- /dev/null +++ b/oak-it/osgi/src/test/java/org/apache/jackrabbit/oak/osgi/OSGiIT.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.osgi; + +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; +import static org.ops4j.pax.exam.CoreOptions.bundle; +import static org.ops4j.pax.exam.CoreOptions.junitBundles; +import static org.ops4j.pax.exam.CoreOptions.mavenBundle; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.regex.Pattern; + +import javax.inject.Inject; +import javax.jcr.Repository; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.CoreOptions; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.junit.Configuration; +import org.ops4j.pax.exam.junit.JUnit4TestRunner; + +@RunWith(JUnit4TestRunner.class) +public class OSGiIT { + + private final File TARGET = new File("target"); + + @Configuration + public Option[] configuration() throws IOException, URISyntaxException { + File base = new File(TARGET, "test-bundles"); + return CoreOptions.options( + junitBundles(), + mavenBundle("org.apache.felix", "org.apache.felix.scr", "1.6.0"), + bundle(new File(base, "jcr.jar").toURI().toURL().toString()), + bundle(new File(base, "guava.jar").toURI().toURL().toString()), + bundle(new File(base, "jackrabbit-api.jar").toURI().toURL().toString()), + bundle(new File(base, "jackrabbit-jcr-commons.jar").toURI().toURL().toString()), + bundle(new File(base, "oak-commons.jar").toURI().toURL().toString()), + bundle(new File(base, "oak-mk-api.jar").toURI().toURL().toString()), + bundle(new File(base, "oak-mk.jar").toURI().toURL().toString()), + bundle(new File(base, "oak-mk-remote.jar").toURI().toURL().toString()), + bundle(new File(base, "oak-core.jar").toURI().toURL().toString()), + bundle(new File(base, "oak-jcr.jar").toURI().toURL().toString())); + } + + @Inject + private MicroKernel kernel; + + @Test + public void testMicroKernel() { + assertNotNull(kernel); + assertTrue(Pattern.matches("[0-9a-f]+", kernel.getHeadRevision())); + } + + @Inject + private ContentRepository oakRepository; + + @Test + public void testOakRepository() { + assertNotNull(oakRepository); + // TODO: try something with oakRepository + } + + @Inject + private Repository jcrRepository; + + @Test + public void testJcrRepository() { + assertNotNull(jcrRepository); + // TODO: try something with jcrRepository + } + +} diff --git a/oak-it/osgi/test-bundles.xml b/oak-it/osgi/test-bundles.xml new file mode 100644 index 00000000000..fcf8da8361a --- /dev/null +++ b/oak-it/osgi/test-bundles.xml @@ -0,0 +1,44 @@ + + + bundles + + dir + + false + + + + ${artifact.artifactId}.jar + test + + javax.jcr:jcr + com.google.guava:guava + org.apache.jackrabbit:jackrabbit-api + org.apache.jackrabbit:jackrabbit-jcr-commons + org.apache.jackrabbit:oak-commons + org.apache.jackrabbit:oak-mk-api + org.apache.jackrabbit:oak-mk + org.apache.jackrabbit:oak-mk-remote + org.apache.jackrabbit:oak-core + org.apache.jackrabbit:oak-jcr + + + + diff --git a/oak-it/pom.xml b/oak-it/pom.xml index 4ce5e95d5c6..39706c6151a 100644 --- a/oak-it/pom.xml +++ b/oak-it/pom.xml @@ -17,24 +17,27 @@ limitations under the License. --> - + 4.0.0 org.apache.jackrabbit oak-parent - 0.1-SNAPSHOT + 0.6-SNAPSHOT ../oak-parent/pom.xml oak-it Oak Integration Tests + pom true + + mk + osgi + + diff --git a/oak-jcr/pom.xml b/oak-jcr/pom.xml new file mode 100644 index 00000000000..866bd1633c9 --- /dev/null +++ b/oak-jcr/pom.xml @@ -0,0 +1,409 @@ + + + + + + 4.0.0 + + + org.apache.jackrabbit + oak-parent + 0.6-SNAPSHOT + ../oak-parent/pom.xml + + + oak-jcr + Oak JCR Binding + bundle + + + + org.apache.jackrabbit.test.api.AddNodeTest#testSameNameSiblings + org.apache.jackrabbit.test.api.SessionTest#testMoveConstraintViolationExceptionSrc + org.apache.jackrabbit.test.api.SessionTest#testMoveConstraintViolationExceptionDest + org.apache.jackrabbit.test.api.SessionTest#testSaveConstraintViolationException + org.apache.jackrabbit.test.api.SessionTest#testHasCapability + org.apache.jackrabbit.test.api.SessionTest#testMoveLockException + org.apache.jackrabbit.test.api.SessionUUIDTest#testSaveReferentialIntegrityException + org.apache.jackrabbit.test.api.SessionUUIDTest#testSaveMovedRefNode + org.apache.jackrabbit.test.api.NodeTest#testSaveConstraintViolationException + org.apache.jackrabbit.test.api.NodeTest#testAddNodeConstraintViolationExceptionUndefinedNodeType + org.apache.jackrabbit.test.api.NodeTest#testRemoveMandatoryNode + org.apache.jackrabbit.test.api.NodeTest#testRefreshInvalidItemStateException + org.apache.jackrabbit.test.api.NodeTest#testRemoveNodeLockedItself + org.apache.jackrabbit.test.api.NodeTest#testRemoveNodeParentLocked + org.apache.jackrabbit.test.api.NodeUUIDTest#testSaveReferentialIntegrityException + org.apache.jackrabbit.test.api.NodeUUIDTest#testSaveMovedRefNode + org.apache.jackrabbit.test.api.NodeOrderableChildNodesTest#testOrderBeforeUnsupportedRepositoryOperationException + org.apache.jackrabbit.test.api.NodeOrderableChildNodesTest#testOrderBeforePlaceAtEndParentSave + org.apache.jackrabbit.test.api.NodeOrderableChildNodesTest#testOrderBeforePlaceAtEndSessionSave + org.apache.jackrabbit.test.api.SetValueValueFormatExceptionTest#testNodeNotReferenceable + org.apache.jackrabbit.test.api.NodeSetPrimaryTypeTest#testLocked + org.apache.jackrabbit.test.api.WorkspaceCopyReferenceableTest#testCopyNodesNewUUID + org.apache.jackrabbit.test.api.WorkspaceCopyVersionableTest#testCopyNodesVersionableAndCheckedIn + org.apache.jackrabbit.test.api.WorkspaceMoveReferenceableTest#testMoveNodesReferenceableNodesNewUUID + org.apache.jackrabbit.test.api.WorkspaceMoveVersionableTest#testMoveNodesVersionableAndCheckedIn + org.apache.jackrabbit.test.api.SessionRemoveItemTest#testRemoveLockedNode + org.apache.jackrabbit.test.api.SessionRemoveItemTest#testRemoveLockedChildItem + org.apache.jackrabbit.test.api.SessionRemoveItemTest#testRemoveCheckedInItem + org.apache.jackrabbit.test.api.SetPropertyAssumeTypeTest#testValueConstraintViolationExceptionBecauseOfInvalidTypeParameter + org.apache.jackrabbit.test.api.SetPropertyAssumeTypeTest#testValuesConstraintViolationExceptionBecauseOfInvalidTypeParameter + org.apache.jackrabbit.test.api.SetPropertyAssumeTypeTest#testStringConstraintViolationExceptionBecauseOfInvalidTypeParameter + org.apache.jackrabbit.test.api.NodeAddMixinTest#testAddInheritedMixin + org.apache.jackrabbit.test.api.NodeAddMixinTest#testLocked + org.apache.jackrabbit.test.api.NodeCanAddMixinTest#testLocked + org.apache.jackrabbit.test.api.NodeRemoveMixinTest#testLocked + org.apache.jackrabbit.test.api.ValueFactoryTest#testValueFormatException + org.apache.jackrabbit.test.api.WorkspaceCopySameNameSibsTest + org.apache.jackrabbit.test.api.WorkspaceCopyTest#testCopyNodesConstraintViolationException + org.apache.jackrabbit.test.api.WorkspaceCopyTest#testCopyNodesAccessDenied + org.apache.jackrabbit.test.api.WorkspaceCopyTest#testCopyNodesLocked + org.apache.jackrabbit.test.api.WorkspaceMoveSameNameSibsTest + org.apache.jackrabbit.test.api.WorkspaceMoveTest#testMoveNodesConstraintViolationException + org.apache.jackrabbit.test.api.WorkspaceMoveTest#testMoveNodesLocked + org.apache.jackrabbit.test.api.WorkspaceMoveTest#testMoveNodesAccessDenied + org.apache.jackrabbit.test.api.CheckPermissionTest + org.apache.jackrabbit.test.api.DocumentViewImportTest + org.apache.jackrabbit.test.api.SerializationTest + org.apache.jackrabbit.test.api.HasPermissionTest + org.apache.jackrabbit.test.api.lock.LockManagerTest#testAddInvalidLockToken + org.apache.jackrabbit.test.api.lock.LockManagerTest#testLockNonLockable + org.apache.jackrabbit.test.api.lock.LockTest#testGetNode + org.apache.jackrabbit.test.api.lock.LockTest#testAddRemoveLockToken + org.apache.jackrabbit.test.api.lock.LockTest#testNodeLocked + org.apache.jackrabbit.test.api.lock.LockTest#testGetLockOwnerProperty + org.apache.jackrabbit.test.api.lock.LockTest#testGetLockOwner + org.apache.jackrabbit.test.api.lock.LockTest#testShallowLock + org.apache.jackrabbit.test.api.lock.LockTest#testParentChildLock + org.apache.jackrabbit.test.api.lock.LockTest#testParentChildDeepLock + org.apache.jackrabbit.test.api.lock.LockTest#testIsDeep + org.apache.jackrabbit.test.api.lock.LockTest#testIsSessionScoped + org.apache.jackrabbit.test.api.lock.LockTest#testLogout + org.apache.jackrabbit.test.api.lock.LockTest#testLockTransfer + org.apache.jackrabbit.test.api.lock.LockTest#testOpenScopedLocks + org.apache.jackrabbit.test.api.lock.LockTest#testRefresh + org.apache.jackrabbit.test.api.lock.LockTest#testRefreshNotLive + org.apache.jackrabbit.test.api.lock.LockTest#testGetLock + org.apache.jackrabbit.test.api.lock.LockTest#testMoveLocked + org.apache.jackrabbit.test.api.lock.SetValueLockExceptionTest#testSetValueLockException + org.apache.jackrabbit.test.api.lock.DeepLockTest#testParentChildDeepLock + org.apache.jackrabbit.test.api.lock.DeepLockTest#testGetNodeOnLockObtainedFromChild + org.apache.jackrabbit.test.api.lock.DeepLockTest#testGetNodeOnLockObtainedFromNewChild + org.apache.jackrabbit.test.api.lock.DeepLockTest#testDeepLockAboveLockedChild + org.apache.jackrabbit.test.api.lock.DeepLockTest#testShallowLockAboveLockedChild + org.apache.jackrabbit.test.api.lock.DeepLockTest#testRemoveLockedChild + org.apache.jackrabbit.test.api.lock.DeepLockTest#testIsLive + org.apache.jackrabbit.test.api.lock.DeepLockTest#testIsDeep + org.apache.jackrabbit.test.api.lock.DeepLockTest#testIsSessionScoped + org.apache.jackrabbit.test.api.lock.DeepLockTest#testRefresh + org.apache.jackrabbit.test.api.lock.DeepLockTest#testRefreshNotLive + org.apache.jackrabbit.test.api.lock.DeepLockTest#testLockHoldingNode + org.apache.jackrabbit.test.api.lock.DeepLockTest#testNodeIsLocked + org.apache.jackrabbit.test.api.lock.DeepLockTest#testNodeHoldsLocked + org.apache.jackrabbit.test.api.lock.DeepLockTest#testLockVisibility + org.apache.jackrabbit.test.api.lock.DeepLockTest#testIsLockOwningSession + org.apache.jackrabbit.test.api.lock.DeepLockTest#testGetSecondsRemaining + org.apache.jackrabbit.test.api.lock.DeepLockTest#testGetSecondsRemainingAfterUnlock + org.apache.jackrabbit.test.api.lock.DeepLockTest#testLockExpiration + org.apache.jackrabbit.test.api.lock.DeepLockTest#testOwnerHint + org.apache.jackrabbit.test.api.lock.DeepLockTest#testUnlock + org.apache.jackrabbit.test.api.lock.DeepLockTest#testUnlockByOtherSession + org.apache.jackrabbit.test.api.lock.DeepLockTest#testIsLockedChild + org.apache.jackrabbit.test.api.lock.DeepLockTest#testIsLockedNewChild + org.apache.jackrabbit.test.api.lock.DeepLockTest#testHoldsLockChild + org.apache.jackrabbit.test.api.lock.DeepLockTest#testHoldsLockNewChild + org.apache.jackrabbit.test.api.lock.DeepLockTest#testGetLockOnChild + org.apache.jackrabbit.test.api.lock.DeepLockTest#testGetLockOnNewChild + org.apache.jackrabbit.test.api.lock.DeepLockTest#testRemoveMixLockableFromLockedNode + org.apache.jackrabbit.test.api.lock.LockManagerTest#testLockTransfer + org.apache.jackrabbit.test.api.lock.LockManagerTest#testLockWithPendingChanges + org.apache.jackrabbit.test.api.lock.LockManagerTest#testNullOwnerHint + org.apache.jackrabbit.test.api.lock.LockManagerTest#testGetLockTokens + org.apache.jackrabbit.test.api.lock.LockManagerTest#testGetLockTokensAfterUnlock + org.apache.jackrabbit.test.api.lock.LockManagerTest#testGetLockTokensSessionScoped + org.apache.jackrabbit.test.api.lock.LockManagerTest#testAddLockToken + org.apache.jackrabbit.test.api.lock.LockManagerTest#testAddLockTokenToAnotherSession + org.apache.jackrabbit.test.api.lock.LockManagerTest#testRemoveLockToken + org.apache.jackrabbit.test.api.lock.LockManagerTest#testRemoveLockToken2 + org.apache.jackrabbit.test.api.lock.LockManagerTest#testRemoveLockToken3 + org.apache.jackrabbit.test.api.lock.LockManagerTest#testRemoveLockTokenTwice + org.apache.jackrabbit.test.api.lock.LockManagerTest#testAddLockTokenAgain + org.apache.jackrabbit.test.api.lock.LockManagerTest#testLockTransfer2 + org.apache.jackrabbit.test.api.lock.LockManagerTest#testLockTransfer3 + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testGetLockToken + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testIsLive + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testIsDeep + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testIsSessionScoped + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testRefresh + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testRefreshNotLive + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testLockHoldingNode + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testNodeIsLocked + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testNodeHoldsLocked + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testLockVisibility + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testIsLockOwningSession + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testGetSecondsRemaining + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testGetSecondsRemainingAfterUnlock + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testLockExpiration + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testOwnerHint + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testUnlock + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testUnlockByOtherSession + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testIsLockedChild + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testIsLockedNewChild + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testHoldsLockChild + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testHoldsLockNewChild + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testGetLockOnChild + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testGetLockOnNewChild + org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testRemoveMixLockableFromLockedNode + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testGetLockToken + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testImplicitUnlock + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testImplicitUnlock2 + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testIsLive + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testIsDeep + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testIsSessionScoped + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testRefresh + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testRefreshNotLive + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testLockHoldingNode + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testNodeIsLocked + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testNodeHoldsLocked + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testLockVisibility + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testIsLockOwningSession + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testGetSecondsRemaining + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testGetSecondsRemainingAfterUnlock + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testLockExpiration + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testOwnerHint + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testUnlock + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testUnlockByOtherSession + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testIsLockedChild + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testIsLockedNewChild + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testHoldsLockChild + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testHoldsLockNewChild + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testGetLockOnChild + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testGetLockOnNewChild + org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testRemoveMixLockableFromLockedNode + org.apache.jackrabbit.test.api.nodetype.PropertyDefTest#testIsMandatory + org.apache.jackrabbit.test.api.LifecycleTest + org.apache.jackrabbit.test.api.query.ElementTest#testElementTestNameTestSomeNTWithSNS + org.apache.jackrabbit.test.api.query.GetPropertyNamesTest#testGetPropertyNames + org.apache.jackrabbit.test.api.query.SaveTest#testConstraintViolationException + org.apache.jackrabbit.test.api.query.SaveTest#testItemExistsException + org.apache.jackrabbit.test.api.query.SimpleSelectionTest#testSingleProperty + org.apache.jackrabbit.test.api.query.SaveTest#testLockException + org.apache.jackrabbit.test.api.query.SQLJoinTest#testJoin + org.apache.jackrabbit.test.api.query.SQLJoinTest#testJoinNtBase + org.apache.jackrabbit.test.api.query.SQLJoinTest#testJoinFilterPrimaryType + org.apache.jackrabbit.test.api.query.SQLJoinTest#testJoinSNS + org.apache.jackrabbit.test.api.query.qom.ColumnTest#testExpandColumnsForNodeType + org.apache.jackrabbit.test.api.query.qom.SelectorTest#testUnknownNodeType + org.apache.jackrabbit.test.api.query.qom.DescendantNodeJoinConditionTest#testInnerJoin + org.apache.jackrabbit.test.api.query.qom.NodeNameTest#testReferenceLiteral + org.apache.jackrabbit.test.api.query.qom.NodeNameTest#testWeakReferenceLiteral + org.apache.jackrabbit.test.api.query.qom.ChildNodeJoinConditionTest#testRightOuterJoin + org.apache.jackrabbit.test.api.query.qom.DescendantNodeJoinConditionTest#testRightOuterJoin + org.apache.jackrabbit.test.api.query.qom.DescendantNodeJoinConditionTest#testLeftOuterJoin + org.apache.jackrabbit.test.api.query.qom.SameNodeJoinConditionTest#testInnerJoin + org.apache.jackrabbit.test.api.query.qom.SameNodeJoinConditionTest#testRightOuterJoin + org.apache.jackrabbit.test.api.query.qom.SameNodeJoinConditionTest#testLeftOuterJoin + org.apache.jackrabbit.test.api.query.qom.SameNodeJoinConditionTest#testInnerJoinWithPath + org.apache.jackrabbit.test.api.query.qom.SameNodeJoinConditionTest#testLeftOuterJoinWithPath + org.apache.jackrabbit.test.api.query.qom.SameNodeJoinConditionTest#testRightOuterJoinWithPath + org.apache.jackrabbit.test.api.observation.EventTest#testGetUserId + org.apache.jackrabbit.test.api.observation.NodeMovedTest#testMoveNode + org.apache.jackrabbit.test.api.observation.NodeMovedTest#testMoveTree + org.apache.jackrabbit.test.api.observation.NodeMovedTest#testMoveWithRemove + org.apache.jackrabbit.test.api.observation.NodeReorderTest#testNodeReorderAddRemove + org.apache.jackrabbit.test.api.observation.NodeReorderTest#testNodeReorderMove + org.apache.jackrabbit.test.api.observation.NodeReorderTest#testNodeReorderSameName + org.apache.jackrabbit.test.api.observation.NodeReorderTest#testNodeReorderSameNameWithRemove + org.apache.jackrabbit.test.api.observation.AddEventListenerTest#testNodeType + org.apache.jackrabbit.test.api.observation.AddEventListenerTest#testNoLocalTrue + org.apache.jackrabbit.test.api.observation.GetIdentifierTest#testNodeAdded + org.apache.jackrabbit.test.api.observation.GetIdentifierTest#testNodeMoved + org.apache.jackrabbit.test.api.observation.GetIdentifierTest#testNodeRemoved + org.apache.jackrabbit.test.api.observation.GetIdentifierTest#testPropertyAdded + org.apache.jackrabbit.test.api.observation.GetIdentifierTest#testPropertyChanged + org.apache.jackrabbit.test.api.observation.GetIdentifierTest#testPropertyRemoved + org.apache.jackrabbit.test.api.observation.GetUserDataTest#testSave + org.apache.jackrabbit.test.api.observation.GetUserDataTest#testWorkspaceOperation + org.apache.jackrabbit.test.api.observation.AddEventListenerTest#testUUID + org.apache.jackrabbit.test.api.observation.LockingTest#testAddLockToNode + org.apache.jackrabbit.test.api.observation.LockingTest#testRemoveLockFromNode + org.apache.jackrabbit.oak.jcr.security.user.EveryoneGroupTest#testMembers + org.apache.jackrabbit.oak.jcr.security.user.UserManagerTest#testFindAuthorizableByAddedProperty + org.apache.jackrabbit.oak.jcr.security.user.UserManagerTest#testFindAuthorizableByRelativePath + org.apache.jackrabbit.oak.jcr.security.user.UserManagerTest#testFindUser + org.apache.jackrabbit.oak.jcr.security.user.UserManagerTest#testFindGroup + org.apache.jackrabbit.oak.jcr.security.user.UserQueryTest + org.apache.jackrabbit.oak.jcr.security.user.UserManagerTest#testGetNewAuthorizable + org.apache.jackrabbit.oak.jcr.security.user.UserManagerTest#testCreateGroupWithExistingPrincipal2 + org.apache.jackrabbit.oak.jcr.security.user.UserManagerTest#testCreateGroupWithExistingPrincipal3 + org.apache.jackrabbit.oak.jcr.security.user.UserManagerTest#testEnforceAuthorizableFolderHierarchy + org.apache.jackrabbit.oak.jcr.security.user.UserManagerTest#testCreateGroupWithExistingPrincipal2 + org.apache.jackrabbit.oak.jcr.security.user.GroupTest#testDeeplyNestedGroups + org.apache.jackrabbit.oak.jcr.security.user.GroupTest#testInheritedMembers + org.apache.jackrabbit.oak.jcr.security.user.GroupTest#testCyclicGroups + org.apache.jackrabbit.oak.jcr.security.user.AuthorizableTest#testRemoveListedAuthorizable + org.apache.jackrabbit.oak.jcr.security.user.AuthorizableTest#testSetPropertyInvalidRelativePath + org.apache.jackrabbit.oak.jcr.security.principal.PrincipalManagerTest#testGetPrincipals + org.apache.jackrabbit.oak.jcr.security.principal.PrincipalManagerTest#testGetGroupPrincipals + org.apache.jackrabbit.oak.jcr.security.principal.PrincipalManagerTest#testGetAllPrincipals + org.apache.jackrabbit.oak.jcr.security.principal.PrincipalManagerTest#testGroupMembers + org.apache.jackrabbit.oak.jcr.security.principal.PrincipalManagerTest#testGroupMembership + org.apache.jackrabbit.oak.jcr.security.principal.PrincipalManagerTest#testGetMembersConsistentWithMembership + org.apache.jackrabbit.oak.jcr.security.principal.PrincipalManagerTest#testFindPrincipal + org.apache.jackrabbit.oak.jcr.security.principal.PrincipalManagerTest#testFindPrincipalByType + org.apache.jackrabbit.oak.jcr.security.principal.PrincipalManagerTest#testFindPrincipalByTypeAll + org.apache.jackrabbit.oak.jcr.security.principal.PrincipalManagerTest#testFindEveryone + + + + + + + org.apache.felix + maven-bundle-plugin + + + + ! + + + org.apache.jackrabbit.oak.jcr.osgi.Activator + + + + + + + + + org.apache.rat + apache-rat-plugin + + + + + + + + + + + + org.osgi + org.osgi.core + provided + + + org.osgi + org.osgi.compendium + provided + + + + javax.jcr + jcr + 2.0 + + + + org.apache.jackrabbit + oak-core + ${project.version} + + + org.apache.jackrabbit + oak-commons + ${project.version} + + + org.apache.jackrabbit + jackrabbit-api + ${jackrabbit.version} + + + org.apache.jackrabbit + jackrabbit-jcr-commons + ${jackrabbit.version} + + + + com.google.guava + guava + ${guava.version} + + + org.slf4j + slf4j-api + 1.6.4 + + + + + com.google.code.findbugs + jsr305 + 2.0.0 + provided + + + + + junit + junit + test + + + ch.qos.logback + logback-classic + 1.0.1 + test + + + com.h2database + h2 + 1.3.158 + test + + + org.apache.jackrabbit + jackrabbit-jcr-tests + ${jackrabbit.version} + test + + + + + org.apache.lucene + lucene-core + 4.0.0-ALPHA + test + + + org.apache.lucene + lucene-analyzers-common + 4.0.0-ALPHA + test + + + org.apache.tika + tika-core + 1.2 + test + + + + diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Descriptors.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Descriptors.java new file mode 100644 index 00000000000..d49d7615229 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Descriptors.java @@ -0,0 +1,306 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.jackrabbit.oak.jcr; + +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.PropertyType; +import javax.jcr.Repository; +import javax.jcr.Value; +import javax.jcr.ValueFactory; + +import static javax.jcr.Repository.IDENTIFIER_STABILITY; +import static javax.jcr.Repository.LEVEL_1_SUPPORTED; +import static javax.jcr.Repository.LEVEL_2_SUPPORTED; +import static javax.jcr.Repository.NODE_TYPE_MANAGEMENT_AUTOCREATED_DEFINITIONS_SUPPORTED; +import static javax.jcr.Repository.NODE_TYPE_MANAGEMENT_INHERITANCE; +import static javax.jcr.Repository.NODE_TYPE_MANAGEMENT_INHERITANCE_SINGLE; +import static javax.jcr.Repository.NODE_TYPE_MANAGEMENT_MULTIPLE_BINARY_PROPERTIES_SUPPORTED; +import static javax.jcr.Repository.NODE_TYPE_MANAGEMENT_MULTIVALUED_PROPERTIES_SUPPORTED; +import static javax.jcr.Repository.NODE_TYPE_MANAGEMENT_ORDERABLE_CHILD_NODES_SUPPORTED; +import static javax.jcr.Repository.NODE_TYPE_MANAGEMENT_OVERRIDES_SUPPORTED; +import static javax.jcr.Repository.NODE_TYPE_MANAGEMENT_PRIMARY_ITEM_NAME_SUPPORTED; +import static javax.jcr.Repository.NODE_TYPE_MANAGEMENT_PROPERTY_TYPES; +import static javax.jcr.Repository.NODE_TYPE_MANAGEMENT_RESIDUAL_DEFINITIONS_SUPPORTED; +import static javax.jcr.Repository.NODE_TYPE_MANAGEMENT_SAME_NAME_SIBLINGS_SUPPORTED; +import static javax.jcr.Repository.NODE_TYPE_MANAGEMENT_UPDATE_IN_USE_SUPORTED; +import static javax.jcr.Repository.NODE_TYPE_MANAGEMENT_VALUE_CONSTRAINTS_SUPPORTED; +import static javax.jcr.Repository.OPTION_ACCESS_CONTROL_SUPPORTED; +import static javax.jcr.Repository.OPTION_ACTIVITIES_SUPPORTED; +import static javax.jcr.Repository.OPTION_BASELINES_SUPPORTED; +import static javax.jcr.Repository.OPTION_JOURNALED_OBSERVATION_SUPPORTED; +import static javax.jcr.Repository.OPTION_LIFECYCLE_SUPPORTED; +import static javax.jcr.Repository.OPTION_LOCKING_SUPPORTED; +import static javax.jcr.Repository.OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED; +import static javax.jcr.Repository.OPTION_NODE_TYPE_MANAGEMENT_SUPPORTED; +import static javax.jcr.Repository.OPTION_OBSERVATION_SUPPORTED; +import static javax.jcr.Repository.OPTION_QUERY_SQL_SUPPORTED; +import static javax.jcr.Repository.OPTION_RETENTION_SUPPORTED; +import static javax.jcr.Repository.OPTION_SHAREABLE_NODES_SUPPORTED; +import static javax.jcr.Repository.OPTION_SIMPLE_VERSIONING_SUPPORTED; +import static javax.jcr.Repository.OPTION_TRANSACTIONS_SUPPORTED; +import static javax.jcr.Repository.OPTION_UNFILED_CONTENT_SUPPORTED; +import static javax.jcr.Repository.OPTION_UPDATE_MIXIN_NODE_TYPES_SUPPORTED; +import static javax.jcr.Repository.OPTION_UPDATE_PRIMARY_NODE_TYPE_SUPPORTED; +import static javax.jcr.Repository.OPTION_VERSIONING_SUPPORTED; +import static javax.jcr.Repository.OPTION_WORKSPACE_MANAGEMENT_SUPPORTED; +import static javax.jcr.Repository.OPTION_XML_EXPORT_SUPPORTED; +import static javax.jcr.Repository.OPTION_XML_IMPORT_SUPPORTED; +import static javax.jcr.Repository.QUERY_FULL_TEXT_SEARCH_SUPPORTED; +import static javax.jcr.Repository.QUERY_JOINS; +import static javax.jcr.Repository.QUERY_JOINS_NONE; +import static javax.jcr.Repository.QUERY_LANGUAGES; +import static javax.jcr.Repository.QUERY_STORED_QUERIES_SUPPORTED; +import static javax.jcr.Repository.QUERY_XPATH_DOC_ORDER; +import static javax.jcr.Repository.QUERY_XPATH_POS_INDEX; +import static javax.jcr.Repository.REP_NAME_DESC; +import static javax.jcr.Repository.REP_VENDOR_DESC; +import static javax.jcr.Repository.REP_VENDOR_URL_DESC; +import static javax.jcr.Repository.SPEC_NAME_DESC; +import static javax.jcr.Repository.SPEC_VERSION_DESC; +import static javax.jcr.Repository.WRITE_SUPPORTED; + +public class Descriptors { + + private final Map descriptors; + + @SuppressWarnings("deprecation") + public Descriptors(ValueFactory valueFactory) { + descriptors = new HashMap(); + Value trueValue = valueFactory.createValue(true); + Value falseValue = valueFactory.createValue(false); + + put(new Descriptor( + IDENTIFIER_STABILITY, + valueFactory.createValue(Repository.IDENTIFIER_STABILITY_METHOD_DURATION), true, true)); + put(new Descriptor( + LEVEL_1_SUPPORTED, + trueValue, true, true)); + put(new Descriptor( + LEVEL_2_SUPPORTED, + trueValue, true, true)); + put(new Descriptor( + OPTION_NODE_TYPE_MANAGEMENT_SUPPORTED, + trueValue, true, true)); + put(new Descriptor( + NODE_TYPE_MANAGEMENT_AUTOCREATED_DEFINITIONS_SUPPORTED, + trueValue, true, true)); + put(new Descriptor( + NODE_TYPE_MANAGEMENT_INHERITANCE, + valueFactory.createValue(NODE_TYPE_MANAGEMENT_INHERITANCE_SINGLE), true, true)); + put(new Descriptor( + NODE_TYPE_MANAGEMENT_MULTIPLE_BINARY_PROPERTIES_SUPPORTED, + trueValue, true, true)); + put(new Descriptor( + NODE_TYPE_MANAGEMENT_MULTIVALUED_PROPERTIES_SUPPORTED, + trueValue, true, true)); + put(new Descriptor( + NODE_TYPE_MANAGEMENT_ORDERABLE_CHILD_NODES_SUPPORTED, + trueValue, true, true)); + put(new Descriptor( + NODE_TYPE_MANAGEMENT_OVERRIDES_SUPPORTED, + trueValue, true, true)); + put(new Descriptor( + NODE_TYPE_MANAGEMENT_PRIMARY_ITEM_NAME_SUPPORTED, + trueValue, true, true)); + put(new Descriptor( + NODE_TYPE_MANAGEMENT_PROPERTY_TYPES, + new Value[] { + valueFactory.createValue(PropertyType.TYPENAME_STRING), + valueFactory.createValue(PropertyType.TYPENAME_BINARY), + valueFactory.createValue(PropertyType.TYPENAME_LONG), + valueFactory.createValue(PropertyType.TYPENAME_LONG), + valueFactory.createValue(PropertyType.TYPENAME_DOUBLE), + valueFactory.createValue(PropertyType.TYPENAME_DECIMAL), + valueFactory.createValue(PropertyType.TYPENAME_DATE), + valueFactory.createValue(PropertyType.TYPENAME_BOOLEAN), + valueFactory.createValue(PropertyType.TYPENAME_NAME), + valueFactory.createValue(PropertyType.TYPENAME_PATH), + valueFactory.createValue(PropertyType.TYPENAME_REFERENCE), + valueFactory.createValue(PropertyType.TYPENAME_WEAKREFERENCE), + valueFactory.createValue(PropertyType.TYPENAME_URI), + valueFactory.createValue(PropertyType.TYPENAME_UNDEFINED) + }, false, true)); + put(new Descriptor( + NODE_TYPE_MANAGEMENT_RESIDUAL_DEFINITIONS_SUPPORTED, + trueValue, true, true)); + put(new Descriptor( + NODE_TYPE_MANAGEMENT_SAME_NAME_SIBLINGS_SUPPORTED, + trueValue, true, true)); + put(new Descriptor( + NODE_TYPE_MANAGEMENT_VALUE_CONSTRAINTS_SUPPORTED, + trueValue, true, true)); + put(new Descriptor( + NODE_TYPE_MANAGEMENT_UPDATE_IN_USE_SUPORTED, + falseValue, true, true)); + put(new Descriptor( + OPTION_ACCESS_CONTROL_SUPPORTED, + falseValue, true, true)); + put(new Descriptor( + OPTION_JOURNALED_OBSERVATION_SUPPORTED, + falseValue, true, true)); + put(new Descriptor( + OPTION_LIFECYCLE_SUPPORTED, + falseValue, true, true)); + put(new Descriptor( + OPTION_LOCKING_SUPPORTED, + trueValue, true, true)); + put(new Descriptor( + OPTION_OBSERVATION_SUPPORTED, + trueValue, true, true)); + put(new Descriptor( + OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED, + falseValue, true, true)); + put(new Descriptor( + OPTION_QUERY_SQL_SUPPORTED, + falseValue, true, true)); + put(new Descriptor( + OPTION_RETENTION_SUPPORTED, + falseValue, true, true)); + put(new Descriptor( + OPTION_SHAREABLE_NODES_SUPPORTED, + falseValue, true, true)); + put(new Descriptor( + OPTION_SIMPLE_VERSIONING_SUPPORTED, + falseValue, true, true)); + put(new Descriptor( + OPTION_TRANSACTIONS_SUPPORTED, + falseValue, true, true)); + put(new Descriptor( + OPTION_UNFILED_CONTENT_SUPPORTED, + falseValue, true, true)); + put(new Descriptor( + OPTION_UPDATE_MIXIN_NODE_TYPES_SUPPORTED, + trueValue, true, true)); + put(new Descriptor( + OPTION_UPDATE_PRIMARY_NODE_TYPE_SUPPORTED, + trueValue, true, true)); + put(new Descriptor( + OPTION_VERSIONING_SUPPORTED, + falseValue, true, true)); + put(new Descriptor( + OPTION_WORKSPACE_MANAGEMENT_SUPPORTED, + trueValue, true, true)); + put(new Descriptor( + OPTION_XML_EXPORT_SUPPORTED, + trueValue, true, true)); + put(new Descriptor( + OPTION_XML_IMPORT_SUPPORTED, + trueValue, true, true)); + put(new Descriptor( + OPTION_ACTIVITIES_SUPPORTED, + falseValue, true, true)); + put(new Descriptor( + OPTION_BASELINES_SUPPORTED, + falseValue, true, true)); + put(new Descriptor( + QUERY_FULL_TEXT_SEARCH_SUPPORTED, + falseValue, true, true)); + put(new Descriptor( + QUERY_JOINS, + valueFactory.createValue(QUERY_JOINS_NONE), true, true)); + put(new Descriptor( + QUERY_LANGUAGES, + new Value[0], false, true)); + put(new Descriptor( + QUERY_STORED_QUERIES_SUPPORTED, + falseValue, true, true)); + put(new Descriptor( + QUERY_XPATH_DOC_ORDER, + falseValue, true, true)); + put(new Descriptor( + QUERY_XPATH_POS_INDEX, + falseValue, true, true)); + put(new Descriptor( + REP_NAME_DESC, + valueFactory.createValue("Apache Jackrabbit Oak JCR implementation"), true, true)); + put(new Descriptor( + REP_VENDOR_DESC, + valueFactory.createValue("Apache Software Foundation"), true, true)); + put(new Descriptor( + REP_VENDOR_URL_DESC, + valueFactory.createValue("http://www.apache.org/"), true, true)); + put(new Descriptor( + SPEC_NAME_DESC, + valueFactory.createValue("Content Repository for Java Technology API"), true, true)); + put(new Descriptor( + SPEC_VERSION_DESC, + valueFactory.createValue("2.0"), true, true)); + put(new Descriptor( + WRITE_SUPPORTED, + trueValue, true, true)); + } + + public Descriptors(ValueFactory valueFactory, Iterable descriptors) { + this(valueFactory); + + for (Descriptor d : descriptors) { + this.descriptors.put(d.name, d); + } + } + + public String[] getKeys() { + return descriptors.keySet().toArray(new String[descriptors.size()]); + } + + public boolean isStandardDescriptor(String key) { + return descriptors.containsKey(key) && descriptors.get(key).standard; + } + + public boolean isSingleValueDescriptor(String key) { + return descriptors.containsKey(key) && descriptors.get(key).singleValued; + } + + public Value getValue(String key) { + Descriptor d = descriptors.get(key); + return d == null || !d.singleValued ? null : d.values[0]; + } + + public Value[] getValues(String key) { + Descriptor d = descriptors.get(key); + return d == null ? null : d.values; + } + + public static final class Descriptor { + final String name; + final Value[] values; + final boolean singleValued; + final boolean standard; + + public Descriptor(String name, Value[] values, boolean singleValued, boolean standard) { + this.name = name; + this.values = values; + this.singleValued = singleValued; + this.standard = standard; + } + + public Descriptor(String name, Value value, boolean singleValued, boolean standard) { + this(name, new Value[]{ value }, singleValued, standard); + } + } + + //------------------------------------------< private >--- + + private void put(Descriptor descriptor) { + descriptors.put(descriptor.name, descriptor); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/ItemDelegate.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/ItemDelegate.java new file mode 100644 index 00000000000..bcedf3b108b --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/ItemDelegate.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.jcr; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.jcr.InvalidItemStateException; + +import org.apache.jackrabbit.oak.api.Tree.Status; +import org.apache.jackrabbit.oak.api.TreeLocation; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.core.TreeImpl.NullLocation; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Abstract base class for {@link NodeDelegate} and {@link PropertyDelegate} + */ +public abstract class ItemDelegate { + + protected final SessionDelegate sessionDelegate; + + /** The underlying {@link org.apache.jackrabbit.oak.api.TreeLocation} of this item. */ + private TreeLocation location; + + ItemDelegate(SessionDelegate sessionDelegate, TreeLocation location) { + this.sessionDelegate = checkNotNull(sessionDelegate); + this.location = checkNotNull(location); + } + + /** + * Get the name of this item + * @return oak name of this item + */ + @Nonnull + public String getName() throws InvalidItemStateException { + return PathUtils.getName(getPath()); + } + + /** + * Get the path of this item + * @return oak path of this item + */ + @Nonnull + public String getPath() throws InvalidItemStateException { + return getLocation().getPath(); // never null + } + + /** + * Get the parent of this item or {@code null}. + * @return parent of this item or {@code null} for root or if the parent + * is not accessible. + */ + @CheckForNull + public NodeDelegate getParent() throws InvalidItemStateException { + return NodeDelegate.create(sessionDelegate, getLocation().getParent()); + } + + /** + * Determine whether this item is stale + * @return {@code true} iff stale + */ + public boolean isStale() { + return getLocationOrNull() == NullLocation.INSTANCE; + } + + /** + * Get the status of this item + * @return {@link Status} of this item + */ + @Nonnull + public Status getStatus() throws InvalidItemStateException { + Status status = getLocation().getStatus(); + if (status == null) { + throw new InvalidItemStateException(); + } + return status; + } + + /** + * Get the session delegate with which this item is associated + * @return {@link SessionDelegate} to which this item belongs + */ + @Nonnull + public final SessionDelegate getSessionDelegate() { + return sessionDelegate; + } + + /** + * The underlying {@link org.apache.jackrabbit.oak.api.TreeLocation} of this item. + * @return tree location of the underlying item + * @throws InvalidItemStateException if the location points to a stale item + */ + @Nonnull + public TreeLocation getLocation() throws InvalidItemStateException { + TreeLocation location = getLocationOrNull(); + if (location == NullLocation.INSTANCE) { + throw new InvalidItemStateException("Item is stale"); + } + return location; + } + + @Override + public String toString() { + // don't disturb the state: avoid resolving location + return getClass().getSimpleName() + '[' + location.getPath() + ']'; + } + + //------------------------------------------------------------< private >--- + + /** + * The underlying {@link org.apache.jackrabbit.oak.api.TreeLocation} of this item. + * @return tree location of the underlying item or {@code null} if stale. + */ + @CheckForNull + private synchronized TreeLocation getLocationOrNull() { + if (location != NullLocation.INSTANCE) { + location = sessionDelegate.getLocation(location.getPath()); + } + return location; + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/ItemImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/ItemImpl.java new file mode 100644 index 00000000000..4e0fcd7a258 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/ItemImpl.java @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr; + +import javax.annotation.Nonnull; +import javax.jcr.InvalidItemStateException; +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.ValueFactory; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.ItemDefinition; + +import org.apache.jackrabbit.commons.AbstractItem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@code ItemImpl}... + */ +abstract class ItemImpl extends AbstractItem { + + protected final SessionDelegate sessionDelegate; + protected final T dlg; + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(ItemImpl.class); + + protected ItemImpl(SessionDelegate sessionDelegate, T itemDelegate) { + this.sessionDelegate = sessionDelegate; + this.dlg = itemDelegate; + } + + //---------------------------------------------------------------< Item >--- + + /** + * @see javax.jcr.Item#getName() + */ + @Override + @Nonnull + public String getName() throws RepositoryException { + return sessionDelegate.perform(new SessionOperation() { + @Override + public String perform() throws RepositoryException { + String oakName = dlg.getName(); + // special case name of root node + return oakName.isEmpty() ? "" : toJcrPath(dlg.getName()); + } + }); + } + + /** + * @see javax.jcr.Property#getPath() + */ + @Override + @Nonnull + public String getPath() throws RepositoryException { + return sessionDelegate.perform(new SessionOperation() { + @Override + public String perform() throws RepositoryException { + return toJcrPath(dlg.getPath()); + } + }); + } + + @Override + @Nonnull + public Session getSession() throws RepositoryException { + return sessionDelegate.getSession(); + } + + /** + * @see Item#isSame(javax.jcr.Item) + */ + @Override + public boolean isSame(Item otherItem) throws RepositoryException { + if (this == otherItem) { + return true; + } + + // The objects are either both Node objects or both Property objects. + if (isNode() != otherItem.isNode()) { + return false; + } + + // Test if both items belong to the same repository + // created by the same Repository object + if (!getSession().getRepository().equals(otherItem.getSession().getRepository())) { + return false; + } + + // Both objects were acquired through Session objects bound to the same + // repository workspace. + if (!getSession().getWorkspace().getName().equals(otherItem.getSession().getWorkspace().getName())) { + return false; + } + + if (isNode()) { + return ((Node) this).getIdentifier().equals(((Node) otherItem).getIdentifier()); + } else { + return getName().equals(otherItem.getName()) && getParent().isSame(otherItem.getParent()); + } + } + + /** + * @see javax.jcr.Item#save() + */ + @Override + public void save() throws RepositoryException { + log.warn("Item#save is no longer supported. Please use Session#save instead."); + + if (isNew()) { + throw new RepositoryException("Item.save() not allowed on new item"); + } + + getSession().save(); + } + + /** + * @see Item#refresh(boolean) + */ + @Override + public void refresh(boolean keepChanges) throws RepositoryException { + log.warn("Item#refresh is no longer supported. Please use Session#refresh"); + getSession().refresh(keepChanges); + } + + @Override + public String toString() { + return (isNode() ? "Node[" : "Property[") + dlg + ']'; + } + + //-----------------------------------------------------------< internal >--- + /** + * Performs a sanity check on this item and the associated session. + * + * @throws RepositoryException if this item has been rendered invalid for some reason + */ + void checkStatus() throws RepositoryException { + if (dlg.isStale()) { + throw new InvalidItemStateException("stale"); + } + + // check session status + if (!sessionDelegate.isAlive()) { + throw new RepositoryException("This session has been closed."); + } + + // TODO: validate item state. + } + + void checkProtected() throws RepositoryException { + ItemDefinition definition = (isNode()) ? ((Node) this).getDefinition() : ((Property) this).getDefinition(); + checkProtected(definition); + } + + void checkProtected(ItemDefinition definition) throws RepositoryException { + if (definition.isProtected()) { + throw new ConstraintViolationException("Item is protected."); + } + } + + /** + * Ensure that the associated session has no pending changes and throw an + * exception otherwise. + * + * @throws InvalidItemStateException if this nodes session has pending changes + * @throws RepositoryException + */ + void ensureNoPendingSessionChanges() throws RepositoryException { + // check for pending changes + if (sessionDelegate.hasPendingChanges()) { + String msg = "Unable to perform operation. Session has pending changes."; + log.debug(msg); + throw new InvalidItemStateException(msg); + } + } + + /** + * Returns the value factory associated with the editing session. + * + * @return the value factory + */ + @Nonnull + ValueFactory getValueFactory() { + return sessionDelegate.getValueFactory(); + } + + @Nonnull + String toJcrPath(String oakPath) { + return sessionDelegate.getNamePathMapper().getJcrPath(oakPath); + } +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Jcr.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Jcr.java new file mode 100644 index 00000000000..6b7df3b6bd9 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Jcr.java @@ -0,0 +1,147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import javax.annotation.Nonnull; +import javax.jcr.Repository; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.plugins.commit.AnnotatingConflictHandler; +import org.apache.jackrabbit.oak.plugins.commit.ConflictValidatorProvider; +import org.apache.jackrabbit.oak.plugins.index.CompositeIndexHookProvider; +import org.apache.jackrabbit.oak.plugins.index.IndexHookManager; +import org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexHookProvider; +import org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexProvider; +import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexHookProvider; +import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexProvider; +import org.apache.jackrabbit.oak.plugins.name.NameValidatorProvider; +import org.apache.jackrabbit.oak.plugins.name.NamespaceValidatorProvider; +import org.apache.jackrabbit.oak.plugins.nodetype.DefaultTypeEditor; +import org.apache.jackrabbit.oak.plugins.nodetype.InitialContent; +import org.apache.jackrabbit.oak.plugins.nodetype.RegistrationValidatorProvider; +import org.apache.jackrabbit.oak.plugins.nodetype.TypeValidatorProvider; +import org.apache.jackrabbit.oak.security.SecurityProviderImpl; +import org.apache.jackrabbit.oak.spi.commit.CommitHook; +import org.apache.jackrabbit.oak.spi.commit.ConflictHandler; +import org.apache.jackrabbit.oak.spi.commit.Validator; +import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; +import org.apache.jackrabbit.oak.spi.lifecycle.RepositoryInitializer; +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class Jcr { + + private final Oak oak; + + private ScheduledExecutorService executor = + Executors.newScheduledThreadPool(0); + + private SecurityProvider securityProvider; + + private Jcr(Oak oak) { + this.oak = oak; + + with(new InitialContent()); + + with(new DefaultTypeEditor()); + + with(new SecurityProviderImpl()); + + with(new NameValidatorProvider()); + with(new NamespaceValidatorProvider()); + with(new TypeValidatorProvider()); + with(new RegistrationValidatorProvider()); + with(new ConflictValidatorProvider()); + + with(new IndexHookManager( + new CompositeIndexHookProvider( + new PropertyIndexHookProvider(), + new LuceneIndexHookProvider()))); + with(new AnnotatingConflictHandler()); + + with(new PropertyIndexProvider()); + with(new LuceneIndexProvider()); + } + + public Jcr() { + this(new Oak()); + } + + public Jcr(MicroKernel kernel) { + this(new Oak(kernel)); + } + + @Nonnull + public Jcr with(@Nonnull RepositoryInitializer initializer) { + oak.with(checkNotNull(initializer)); + return this; + } + + @Nonnull + public Jcr with(@Nonnull QueryIndexProvider provider) { + oak.with(checkNotNull(provider)); + return this; + } + + @Nonnull + public Jcr with(@Nonnull CommitHook hook) { + oak.with(checkNotNull(hook)); + return this; + } + + @Nonnull + public Jcr with(@Nonnull ValidatorProvider provider) { + oak.with(checkNotNull(provider)); + return this; + } + + @Nonnull + public Jcr with(@Nonnull Validator validator) { + oak.with(checkNotNull(validator)); + return this; + } + + @Nonnull + public Jcr with(@Nonnull SecurityProvider securityProvider) { + oak.with(checkNotNull(securityProvider)); + this.securityProvider = securityProvider; + return this; + } + + @Nonnull + public Jcr with(@Nonnull ConflictHandler conflictHandler) { + oak.with(checkNotNull(conflictHandler)); + return this; + } + + @Nonnull + public Jcr with(@Nonnull ScheduledExecutorService executor) { + this.executor = checkNotNull(executor); + return this; + } + + public Repository createRepository() { + return new RepositoryImpl( + oak.createContentRepository(), executor, securityProvider); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/NodeDelegate.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/NodeDelegate.java new file mode 100644 index 00000000000..644096bcd52 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/NodeDelegate.java @@ -0,0 +1,282 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr; + +import java.util.Collections; +import java.util.Iterator; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; + +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterators; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.api.TreeLocation; +import org.apache.jackrabbit.oak.plugins.memory.PropertyStates; + +/** + * {@code NodeDelegate} serve as internal representations of {@code Node}s. + * Most methods of this class throw an {@code InvalidItemStateException} + * exception if the instance is stale. An instance is stale if the underlying + * items does not exist anymore. + */ +public class NodeDelegate extends ItemDelegate { + + /** + * Create a new {@code NodeDelegate} instance for a valid {@code TreeLocation}. That + * is for one where {@code getTree() != null}. + * @param sessionDelegate + * @param location + * @return + */ + static NodeDelegate create(SessionDelegate sessionDelegate, TreeLocation location) { + return location.getTree() == null + ? null + : new NodeDelegate(sessionDelegate, location); + } + + NodeDelegate(SessionDelegate sessionDelegate, Tree tree) { + super(sessionDelegate, tree.getLocation()); + } + + private NodeDelegate(SessionDelegate sessionDelegate, TreeLocation location) { + super(sessionDelegate, location); + } + + @Nonnull + public String getIdentifier() throws InvalidItemStateException { + return sessionDelegate.getIdManager().getIdentifier(getTree()); + } + + /** + * Determine whether this is the root node + * @return {@code true} iff this is the root node + */ + public boolean isRoot() throws InvalidItemStateException { + return getTree().isRoot(); + } + + /** + * Get the number of properties of the node + * @return number of properties of the node + */ + public long getPropertyCount() throws InvalidItemStateException { + // TODO: Exclude "invisible" internal properties (OAK-182) + return getTree().getPropertyCount(); + } + + /** + * Get a property + * @param relPath oak path + * @return property at the path given by {@code relPath} or {@code null} if + * no such property exists + */ + @CheckForNull + public PropertyDelegate getProperty(String relPath) throws InvalidItemStateException { + TreeLocation propertyLocation = getChildLocation(relPath); + PropertyState propertyState = propertyLocation.getProperty(); + return propertyState == null + ? null + : new PropertyDelegate(sessionDelegate, propertyLocation); + } + + /** + * Get the properties of the node + * @return properties of the node + */ + @Nonnull + public Iterator getProperties() throws InvalidItemStateException { + return propertyDelegateIterator(getTree().getProperties().iterator()); + } + + /** + * Get the number of child nodes + * @return number of child nodes of the node + */ + public long getChildCount() throws InvalidItemStateException { + // TODO: Exclude "invisible" internal child nodes (OAK-182) + return getTree().getChildrenCount(); + } + + /** + * Get child node + * @param relPath oak path + * @return node at the path given by {@code relPath} or {@code null} if + * no such node exists + */ + @CheckForNull + public NodeDelegate getChild(String relPath) throws InvalidItemStateException { + return create(sessionDelegate, getChildLocation(relPath)); + } + + /** + * Returns an iterator for traversing all the children of this node. + * If the node is orderable then the iterator will return child nodes in the + * specified order. Otherwise the ordering of the iterator is undefined. + * + * @return child nodes of the node + */ + @Nonnull + public Iterator getChildren() throws InvalidItemStateException { + Tree tree = getTree(); + long count = tree.getChildrenCount(); + if (count == 0) { + // Optimise the most common case + return Collections.emptySet().iterator(); + } else if (count == 1) { + // Optimise another typical case + Tree child = tree.getChildren().iterator().next(); + if (!child.getName().startsWith(":")) { + NodeDelegate delegate = new NodeDelegate(sessionDelegate, child); + return Collections.singleton(delegate).iterator(); + } else { + return Collections.emptySet().iterator(); + } + } else { + return nodeDelegateIterator(tree.getChildren().iterator()); + } + } + + public void orderBefore(String source, String target) + throws ItemNotFoundException, InvalidItemStateException { + Tree tree = getTree(); + if (tree.getChild(source) == null) { + throw new ItemNotFoundException("Not a child: " + source); + } else if (target != null && tree.getChild(target) == null) { + throw new ItemNotFoundException("Not a child: " + target); + } else { + tree.getChild(source).orderBefore(target); + } + } + + /** + * Set a property + * @param name oak name + * @param value + * @return the set property + */ + @Nonnull + public PropertyDelegate setProperty(String name, Value value) throws RepositoryException { + Tree tree = getTree(); + PropertyState old = tree.getProperty(name); + if (old != null && old.isArray()) { + throw new ValueFormatException("Attempt to set a single value to multi-valued property."); + } + tree.setProperty(PropertyStates.createProperty(name, value)); + return new PropertyDelegate(sessionDelegate, tree.getLocation().getChild(name)); + } + + public void removeProperty(String name) throws InvalidItemStateException { + getTree().removeProperty(name); + } + + /** + * Set a multi valued property + * @param name oak name + * @param values + * @return the set property + */ + @Nonnull + public PropertyDelegate setProperty(String name, Iterable values) throws RepositoryException { + Tree tree = getTree(); + PropertyState old = tree.getProperty(name); + if (old != null && ! old.isArray()) { + throw new ValueFormatException("Attempt to set multiple values to single valued property."); + } + tree.setProperty(PropertyStates.createProperty(name, values)); + return new PropertyDelegate(sessionDelegate, tree.getLocation().getChild(name)); + } + + /** + * Add a child node + * @param name oak name + * @return the added node or {@code null} if such a node already exists + */ + @CheckForNull + public NodeDelegate addChild(String name) throws InvalidItemStateException { + Tree tree = getTree(); + return tree.hasChild(name) + ? null + : new NodeDelegate(sessionDelegate, tree.addChild(name)); + } + + /** + * Remove the node if not root. Does nothing otherwise + */ + public void remove() throws InvalidItemStateException { + getTree().remove(); + } + + //------------------------------------------------------------< internal >--- + + @Nonnull + Tree getTree() throws InvalidItemStateException { + Tree tree = getLocation().getTree(); + if (tree == null) { + throw new InvalidItemStateException(); + } + return tree; + } + + // -----------------------------------------------------------< private >--- + + private TreeLocation getChildLocation(String relPath) throws InvalidItemStateException { + return getLocation().getChild(relPath); + } + + private Iterator nodeDelegateIterator( + Iterator children) { + return Iterators.transform( + Iterators.filter(children, new Predicate() { + @Override + public boolean apply(Tree tree) { + return !tree.getName().startsWith(":"); + } + }), + new Function() { + @Override + public NodeDelegate apply(Tree tree) { + return new NodeDelegate(sessionDelegate, tree); + } + }); + } + + private Iterator propertyDelegateIterator( + Iterator properties) throws InvalidItemStateException { + final TreeLocation location = getLocation(); + return Iterators.transform( + Iterators.filter(properties, new Predicate() { + @Override + public boolean apply(PropertyState property) { + return !property.getName().startsWith(":"); + } + }), + new Function() { + @Override + public PropertyDelegate apply(PropertyState propertyState) { + return new PropertyDelegate(sessionDelegate, location.getChild(propertyState.getName())); + } + }); + } +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/NodeImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/NodeImpl.java new file mode 100644 index 00000000000..ecc7ab23c8e --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/NodeImpl.java @@ -0,0 +1,1536 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.jcr.AccessDeniedException; +import javax.jcr.Binary; +import javax.jcr.InvalidItemStateException; +import javax.jcr.Item; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.ItemVisitor; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.lock.Lock; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; + +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; +import com.google.common.collect.Lists; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.commons.ItemNameMatcher; +import org.apache.jackrabbit.commons.iterator.NodeIteratorAdapter; +import org.apache.jackrabbit.commons.iterator.PropertyIteratorAdapter; +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.api.Tree.Status; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager; +import org.apache.jackrabbit.oak.plugins.nodetype.DefinitionProvider; +import org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants; +import org.apache.jackrabbit.oak.util.TODO; +import org.apache.jackrabbit.value.ValueHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static javax.jcr.Property.JCR_LOCK_IS_DEEP; +import static javax.jcr.Property.JCR_LOCK_OWNER; + +/** + * {@code NodeImpl}... + */ +public class NodeImpl extends ItemImpl implements Node { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(NodeImpl.class); + + public NodeImpl(NodeDelegate dlg) { + super(dlg.getSessionDelegate(), dlg); + } + + //---------------------------------------------------------------< Item >--- + + /** + * @see javax.jcr.Item#isNode() + */ + @Override + public boolean isNode() { + return true; + } + + /** + * @see javax.jcr.Item#getParent() + */ + @Override + @Nonnull + public Node getParent() throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public NodeImpl perform() throws RepositoryException { + if (dlg.isRoot()) { + throw new ItemNotFoundException("Root has no parent"); + } else { + NodeDelegate parent = dlg.getParent(); + if (parent == null) { + throw new AccessDeniedException(); + } + return new NodeImpl(parent); + } + } + }); + } + + /** + * @see javax.jcr.Item#isNew() + */ + @Override + public boolean isNew() { + try { + return sessionDelegate.perform(new SessionOperation() { + @Override + public Boolean perform() throws InvalidItemStateException { + return !dlg.isStale() && dlg.getStatus() == Status.NEW; + } + }); + } + catch (RepositoryException e) { + return false; + } + } + + /** + * @see javax.jcr.Item#isModified() + */ + @Override + public boolean isModified() { + try { + return sessionDelegate.perform(new SessionOperation() { + @Override + public Boolean perform() throws InvalidItemStateException { + return !dlg.isStale() && dlg.getStatus() == Status.MODIFIED; + } + }); + } + catch (RepositoryException e) { + return false; + } + } + + /** + * @see javax.jcr.Item#remove() + */ + @Override + public void remove() throws RepositoryException { + checkStatus(); + checkProtected(); + + sessionDelegate.perform(new SessionOperation() { + @Override + public Void perform() throws RepositoryException { + if (dlg.isRoot()) { + throw new RepositoryException("Cannot remove the root node"); + } + + dlg.remove(); + return null; + } + }); + } + + /** + * @see Item#accept(javax.jcr.ItemVisitor) + */ + @Override + public void accept(ItemVisitor visitor) throws RepositoryException { + checkStatus(); + visitor.visit(this); + } + + //---------------------------------------------------------------< Node >--- + /** + * @see Node#addNode(String) + */ + @Override + @Nonnull + public Node addNode(String relPath) throws RepositoryException { + checkStatus(); + return addNode(relPath, null); + } + + @Override + @Nonnull + public Node addNode(final String relPath, final String primaryNodeTypeName) throws RepositoryException { + checkStatus(); + checkProtected(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public Node perform() throws RepositoryException { + String oakPath = sessionDelegate.getOakPathKeepIndexOrThrowNotFound(relPath); + String oakName = PathUtils.getName(oakPath); + String parentPath = sessionDelegate.getOakPathOrThrow(PathUtils.getParentPath(oakPath)); + + // handle index + if (oakName.contains("[")) { + throw new RepositoryException("Cannot create a new node using a name including an index"); + } + + NodeDelegate parent = dlg.getChild(parentPath); + if (parent == null) { + // is it a property? + String grandParentPath = PathUtils.getParentPath(parentPath); + NodeDelegate grandParent = dlg.getChild(grandParentPath); + if (grandParent != null) { + String propName = PathUtils.getName(parentPath); + if (grandParent.getProperty(propName) != null) { + throw new ConstraintViolationException("Can't add new node to property."); + } + } + + throw new PathNotFoundException(relPath); + } + + if (parent.getChild(oakName) != null) { + throw new ItemExistsException(relPath); + } + + String ntName = primaryNodeTypeName; + if (ntName == null) { + DefinitionProvider dp = sessionDelegate.getDefinitionProvider(); + try { + ntName = dp.getDefinition(new NodeImpl(parent), + PathUtils.getName(relPath)).getDefaultPrimaryTypeName(); + } catch (RepositoryException e) { + throw new ConstraintViolationException( + "no matching child node definition found for " + relPath); + } + } + + // TODO: figure out the right place for this check + NodeTypeManager ntm = sessionDelegate.getNodeTypeManager(); + NodeType nt = ntm.getNodeType(ntName); // throws on not found + if (nt.isAbstract() || nt.isMixin()) { + throw new ConstraintViolationException(); + } + // TODO: END + + NodeDelegate added = parent.addChild(oakName); + if (added == null) { + throw new ItemExistsException(); + } + + NodeImpl childNode = new NodeImpl(added); + childNode.internalSetPrimaryType(ntName); + childNode.autoCreateItems(); + return childNode; + } + }); + } + + @Override + public void orderBefore(final String srcChildRelPath, final String destChildRelPath) throws RepositoryException { + checkStatus(); + checkProtected(); + + sessionDelegate.perform(new SessionOperation() { + @Override + public Void perform() throws RepositoryException { + String oakSrcChildRelPath = + sessionDelegate.getOakPathOrThrowNotFound(srcChildRelPath); + String oakDestChildRelPath = null; + if (destChildRelPath != null) { + oakDestChildRelPath = + sessionDelegate.getOakPathOrThrowNotFound(destChildRelPath); + } + dlg.orderBefore(oakSrcChildRelPath, oakDestChildRelPath); + return null; + } + }); + } + + /** + * @see Node#setProperty(String, javax.jcr.Value) + */ + @Override + @CheckForNull + public Property setProperty(String name, Value value) throws RepositoryException { + int type = PropertyType.UNDEFINED; + if (value != null) { + type = value.getType(); + } + return internalSetProperty(name, value, type, false); + } + + /** + * @see Node#setProperty(String, javax.jcr.Value, int) + */ + @Override + @Nonnull + public Property setProperty(String name, Value value, int type) + throws RepositoryException { + return internalSetProperty(name, value, type, type != PropertyType.UNDEFINED); + } + + /** + * @see Node#setProperty(String, javax.jcr.Value[]) + */ + @Override + @Nonnull + public Property setProperty(String name, Value[] values) throws RepositoryException { + int type; + if (values == null || values.length == 0 || values[0] == null) { + type = PropertyType.UNDEFINED; + } else { + type = values[0].getType(); + } + return internalSetProperty(name, values, type, false); + } + + @Override + @Nonnull + public Property setProperty(String jcrName, Value[] values, int type) throws RepositoryException { + return internalSetProperty(jcrName, values, type, true); + } + + /** + * @see Node#setProperty(String, String[]) + */ + @Override + @Nonnull + public Property setProperty(String name, String[] values) throws RepositoryException { + return setProperty(name, values, PropertyType.UNDEFINED); + } + + /** + * @see Node#setProperty(String, String[], int) + */ + @Override + @Nonnull + public Property setProperty(String name, String[] values, int type) throws RepositoryException { + Value[] vs; + if (type == PropertyType.UNDEFINED) { + vs = ValueHelper.convert(values, PropertyType.STRING, getValueFactory()); + } else { + vs = ValueHelper.convert(values, type, getValueFactory()); + } + return internalSetProperty(name, vs, type, (type != PropertyType.UNDEFINED)); + } + + /** + * @see Node#setProperty(String, String) + */ + @Override + @CheckForNull + public Property setProperty(String name, String value) throws RepositoryException { + Value v = (value == null) ? null : getValueFactory().createValue(value, PropertyType.STRING); + return internalSetProperty(name, v, PropertyType.UNDEFINED, false); + } + + /** + * @see Node#setProperty(String, String, int) + */ + @Override + @CheckForNull + public Property setProperty(String name, String value, int type) throws RepositoryException { + Value v = (value == null) ? null : getValueFactory().createValue(value, type); + return internalSetProperty(name, v, type, true); + } + + /** + * @see Node#setProperty(String, InputStream) + */ + @SuppressWarnings("deprecation") + @Override + @CheckForNull + public Property setProperty(String name, InputStream value) throws RepositoryException { + Value v = (value == null ? null : getValueFactory().createValue(value)); + return setProperty(name, v, PropertyType.BINARY); + } + + /** + * @see Node#setProperty(String, Binary) + */ + @Override + @CheckForNull + public Property setProperty(String name, Binary value) throws RepositoryException { + Value v = (value == null ? null : getValueFactory().createValue(value)); + return setProperty(name, v, PropertyType.BINARY); + } + + /** + * @see Node#setProperty(String, boolean) + */ + @Override + @Nonnull + public Property setProperty(String name, boolean value) throws RepositoryException { + return setProperty(name, getValueFactory().createValue(value), PropertyType.BOOLEAN); + } + + /** + * @see Node#setProperty(String, double) + */ + @Override + @Nonnull + public Property setProperty(String name, double value) throws RepositoryException { + return setProperty(name, getValueFactory().createValue(value), PropertyType.DOUBLE); + } + + /** + * @see Node#setProperty(String, BigDecimal) + */ + @Override + @CheckForNull + public Property setProperty(String name, BigDecimal value) throws RepositoryException { + Value v = (value == null ? null : getValueFactory().createValue(value)); + return setProperty(name, v, PropertyType.DECIMAL); + } + + /** + * @see Node#setProperty(String, long) + */ + @Override + @Nonnull + public Property setProperty(String name, long value) throws RepositoryException { + return setProperty(name, getValueFactory().createValue(value), PropertyType.LONG); + } + + /** + * @see Node#setProperty(String, Calendar) + */ + @Override + @CheckForNull + public Property setProperty(String name, Calendar value) throws RepositoryException { + Value v = (value == null ? null : getValueFactory().createValue(value)); + return setProperty(name, v, PropertyType.DATE); + } + + /** + * @see Node#setProperty(String, Node) + */ + @Override + @CheckForNull + public Property setProperty(String name, Node value) throws RepositoryException { + Value v = (value == null) ? null : getValueFactory().createValue(value); + return setProperty(name, v, PropertyType.REFERENCE); + } + + @Override + @Nonnull + public Node getNode(final String relPath) throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public NodeImpl perform() throws RepositoryException { + String oakPath = sessionDelegate.getOakPathOrThrowNotFound(relPath); + + NodeDelegate nd = dlg.getChild(oakPath); + if (nd == null) { + throw new PathNotFoundException(relPath); + } else { + return new NodeImpl(nd); + } + } + }); + } + + @Override + @Nonnull + public NodeIterator getNodes() throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public NodeIterator perform() throws RepositoryException { + Iterator children = dlg.getChildren(); + long size = dlg.getChildCount(); + return new NodeIteratorAdapter(nodeIterator(children), size); + } + }); + } + + @Override + @Nonnull + public NodeIterator getNodes(final String namePattern) + throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public NodeIterator perform() throws RepositoryException { + Iterator children = Iterators.filter(dlg.getChildren(), + new Predicate() { + @Override + public boolean apply(NodeDelegate state) { + try { + return ItemNameMatcher.matches(toJcrPath(state.getName()), namePattern); + } catch (InvalidItemStateException e) { + return false; + } + } + }); + + return new NodeIteratorAdapter(nodeIterator(children)); + } + }); + } + + @Override + @Nonnull + public NodeIterator getNodes(final String[] nameGlobs) throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public NodeIterator perform() throws RepositoryException { + Iterator children = Iterators.filter(dlg.getChildren(), + new Predicate() { + @Override + public boolean apply(NodeDelegate state) { + try { + return ItemNameMatcher.matches(toJcrPath(state.getName()), nameGlobs); + } catch (InvalidItemStateException e) { + return false; + } + } + }); + + return new NodeIteratorAdapter(nodeIterator(children)); + } + }); + } + + @Override + @Nonnull + public Property getProperty(final String relPath) throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public PropertyImpl perform() throws RepositoryException { + String oakPath = sessionDelegate.getOakPathOrThrowNotFound(relPath); + PropertyDelegate pd = dlg.getProperty(oakPath); + if (pd == null) { + throw new PathNotFoundException(relPath + " not found on " + getPath()); + } else { + return new PropertyImpl(pd); + } + } + }); + } + + @Override + @Nonnull + public PropertyIterator getProperties() throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public PropertyIterator perform() throws RepositoryException { + Iterator properties = dlg.getProperties(); + long size = dlg.getPropertyCount(); + return new PropertyIteratorAdapter(propertyIterator(properties), size); + } + }); + } + + @Override + @Nonnull + public PropertyIterator getProperties(final String namePattern) throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public PropertyIterator perform() throws RepositoryException { + Iterator properties = Iterators.filter(dlg.getProperties(), + new Predicate() { + @Override + public boolean apply(PropertyDelegate entry) { + try { + return ItemNameMatcher.matches(toJcrPath(entry.getName()), namePattern); + } catch (InvalidItemStateException e) { + return false; + } + } + }); + + return new PropertyIteratorAdapter(propertyIterator(properties)); + } + }); + } + + @Override + @Nonnull + public PropertyIterator getProperties(final String[] nameGlobs) throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public PropertyIterator perform() throws RepositoryException { + Iterator propertyNames = Iterators.filter(dlg.getProperties(), + new Predicate() { + @Override + public boolean apply(PropertyDelegate entry) { + try { + return ItemNameMatcher.matches(toJcrPath(entry.getName()), nameGlobs); + } catch (InvalidItemStateException e) { + return false; + } + } + }); + + return new PropertyIteratorAdapter(propertyIterator(propertyNames)); + } + }); + } + + /** + * @see javax.jcr.Node#getPrimaryItem() + */ + @Override + @Nonnull + public Item getPrimaryItem() throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public Item perform() throws RepositoryException { + String name = getPrimaryNodeType().getPrimaryItemName(); + if (name == null) { + throw new ItemNotFoundException("No primary item present on node " + this); + } + if (hasProperty(name)) { + return getProperty(name); + } else if (hasNode(name)) { + return getNode(name); + } else { + throw new ItemNotFoundException("Primary item " + name + " does not exist on node " + this); + } + } + }); + } + + /** + * @see javax.jcr.Node#getUUID() + */ + @Override + @Nonnull + public String getUUID() throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public String perform() throws RepositoryException { + if (isNodeType(NodeType.MIX_REFERENCEABLE)) { + return getIdentifier(); + } + + throw new UnsupportedRepositoryOperationException("Node is not referenceable."); + } + }); + } + + @Override + @Nonnull + public String getIdentifier() throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public String perform() throws RepositoryException { + return dlg.getIdentifier(); + } + }); + } + + @Override + public int getIndex() throws RepositoryException { + // as long as we do not support same name siblings, index always is 1 + return 1; + } + + /** + * @see javax.jcr.Node#getReferences() + */ + @Override + @Nonnull + public PropertyIterator getReferences() throws RepositoryException { + return getReferences(null); + } + + @Override + @Nonnull + public PropertyIterator getReferences(final String name) throws RepositoryException { + checkStatus(); + return internalGetReferences(name, false); + } + + /** + * @see javax.jcr.Node#getWeakReferences() + */ + @Override + @Nonnull + public PropertyIterator getWeakReferences() throws RepositoryException { + return getWeakReferences(null); + } + + @Override + @Nonnull + public PropertyIterator getWeakReferences(String name) throws RepositoryException { + checkStatus(); + return internalGetReferences(name, true); + } + + private PropertyIterator internalGetReferences(final String name, final boolean weak) throws RepositoryException { + return sessionDelegate.perform(new SessionOperation() { + @Override + public PropertyIterator perform() throws InvalidItemStateException { + IdentifierManager idManager = sessionDelegate.getIdManager(); + + Set propertyOakPaths = idManager.getReferences(weak, dlg.getTree(), name); + Iterable properties = Iterables.transform( + propertyOakPaths, + new Function() { + @Override + public Property apply(String oakPath) { + PropertyDelegate pd = sessionDelegate.getProperty(oakPath); + return pd == null ? null : new PropertyImpl(pd); + } + } + ); + + return new PropertyIteratorAdapter(properties.iterator(), propertyOakPaths.size()); + } + }); + } + + @Override + public boolean hasNode(final String relPath) throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public Boolean perform() throws RepositoryException { + String oakPath = sessionDelegate.getOakPathOrThrow(relPath); + return dlg.getChild(oakPath) != null; + } + }); + } + + @Override + public boolean hasProperty(final String relPath) throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public Boolean perform() throws RepositoryException { + String oakPath = sessionDelegate.getOakPathOrThrow(relPath); + return dlg.getProperty(oakPath) != null; + } + }); + } + + @Override + public boolean hasNodes() throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public Boolean perform() throws RepositoryException { + return dlg.getChildCount() != 0; + } + }); + } + + @Override + public boolean hasProperties() throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public Boolean perform() throws RepositoryException { + return dlg.getPropertyCount() != 0; + } + }); + } + + /** + * @see javax.jcr.Node#getPrimaryNodeType() + */ + @Override + @Nonnull + public NodeType getPrimaryNodeType() throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public NodeType perform() throws RepositoryException { + // TODO: check if transient changes to mixin-types are reflected here + NodeTypeManager ntMgr = sessionDelegate.getNodeTypeManager(); + String primaryNtName; + primaryNtName = hasProperty(Property.JCR_PRIMARY_TYPE) + ? getProperty(Property.JCR_PRIMARY_TYPE).getString() + : NodeType.NT_UNSTRUCTURED; + + return ntMgr.getNodeType(primaryNtName); + } + }); + } + + /** + * @see javax.jcr.Node#getMixinNodeTypes() + */ + @Override + @Nonnull + public NodeType[] getMixinNodeTypes() throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public NodeType[] perform() throws RepositoryException { + // TODO: check if transient changes to mixin-types are reflected here + if (hasProperty(Property.JCR_MIXIN_TYPES)) { + NodeTypeManager ntMgr = sessionDelegate.getNodeTypeManager(); + Value[] mixinNames = getProperty(Property.JCR_MIXIN_TYPES).getValues(); + NodeType[] mixinTypes = new NodeType[mixinNames.length]; + for (int i = 0; i < mixinNames.length; i++) { + mixinTypes[i] = ntMgr.getNodeType(mixinNames[i].getString()); + } + return mixinTypes; + } else { + return new NodeType[0]; + } + } + }); + } + + @Override + public boolean isNodeType(final String nodeTypeName) throws RepositoryException { + checkStatus(); + + // TODO: figure out the right place for this check + NodeTypeManager ntm = sessionDelegate.getNodeTypeManager(); + NodeType ntToCheck = ntm.getNodeType(nodeTypeName); // throws on not found + String nameToCheck = ntToCheck.getName(); + + NodeType currentPrimaryType = getPrimaryNodeType(); + if (currentPrimaryType.isNodeType(nameToCheck)) { + return true; + } + + for (NodeType mixin : getMixinNodeTypes()) { + if (mixin.isNodeType(nameToCheck)) { + return true; + } + } + // TODO: END + + return false; + } + + @Override + public void setPrimaryType(final String nodeTypeName) throws RepositoryException { + checkStatus(); + checkProtected(); + + internalSetPrimaryType(nodeTypeName); + } + + @Override + public void addMixin(final String mixinName) throws RepositoryException { + checkStatus(); + checkProtected(); + + sessionDelegate.perform(new SessionOperation() { + @Override + public Void perform() throws RepositoryException { + // TODO: figure out the right place for this check + NodeTypeManager ntm = sessionDelegate.getNodeTypeManager(); + ntm.getNodeType(mixinName); // throws on not found + // TODO: END + + PropertyDelegate mixins = dlg.getProperty(JcrConstants.JCR_MIXINTYPES); + Value value = sessionDelegate.getValueFactory().createValue(mixinName, PropertyType.NAME); + + boolean nodeModified = false; + if (mixins == null) { + nodeModified = true; + dlg.setProperty(JcrConstants.JCR_MIXINTYPES, Collections.singletonList(value)); + } else { + List values = mixins.getValues(); + if (!values.contains(value)) { + values.add(value); + nodeModified = true; + dlg.setProperty(JcrConstants.JCR_MIXINTYPES, values); + } + } + + if (nodeModified) { + autoCreateItems(); + } + return null; + } + }); + } + + @Override + public void removeMixin(final String mixinName) throws RepositoryException { + checkStatus(); + checkProtected(); + + sessionDelegate.perform(new SessionOperation() { + @Override + public Void perform() throws RepositoryException { + if (!isNodeType(mixinName)) { + throw new NoSuchNodeTypeException(); + } + + throw new ConstraintViolationException(); + } + }); + } + + @Override + public boolean canAddMixin(final String mixinName) throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public Boolean perform() throws RepositoryException { + // TODO: figure out the right place for this check + NodeTypeManager ntm = sessionDelegate.getNodeTypeManager(); + ntm.getNodeType(mixinName); // throws on not found + // TODO: END + + return isSupportedMixinName(mixinName); + } + }); + } + + @Override + @Nonnull + public NodeDefinition getDefinition() throws RepositoryException { + if (getDepth() == 0) { + return dlg.sessionDelegate.getDefinitionProvider().getRootDefinition(); + } else { + return dlg.sessionDelegate.getDefinitionProvider().getDefinition(getParent(), this); + } + } + + @Override + @Nonnull + public String getCorrespondingNodePath(String workspaceName) throws RepositoryException { + checkStatus(); + checkValidWorkspace(workspaceName); + throw new UnsupportedRepositoryOperationException("TODO: Node.getCorrespondingNodePath"); + } + + + @Override + public void update(String srcWorkspace) throws RepositoryException { + checkStatus(); + checkValidWorkspace(srcWorkspace); + ensureNoPendingSessionChanges(); + + // TODO + } + + /** + * @see javax.jcr.Node#checkin() + */ + @Override + @Nonnull + public Version checkin() throws RepositoryException { + return sessionDelegate.getVersionManager().checkin(getPath()); + } + + /** + * @see javax.jcr.Node#checkout() + */ + @Override + public void checkout() throws RepositoryException { + sessionDelegate.getVersionManager().checkout(getPath()); + } + + /** + * @see javax.jcr.Node#doneMerge(javax.jcr.version.Version) + */ + @Override + public void doneMerge(Version version) throws RepositoryException { + sessionDelegate.getVersionManager().doneMerge(getPath(), version); + } + + /** + * @see javax.jcr.Node#cancelMerge(javax.jcr.version.Version) + */ + @Override + public void cancelMerge(Version version) throws RepositoryException { + sessionDelegate.getVersionManager().cancelMerge(getPath(), version); + } + + /** + * @see javax.jcr.Node#merge(String, boolean) + */ + @Override + @Nonnull + public NodeIterator merge(String srcWorkspace, boolean bestEffort) throws RepositoryException { + return sessionDelegate.getVersionManager().merge(getPath(), srcWorkspace, bestEffort); + } + + /** + * @see javax.jcr.Node#isCheckedOut() + */ + @Override + public boolean isCheckedOut() throws RepositoryException { + try { + return sessionDelegate.getVersionManager().isCheckedOut(getPath()); + } catch (UnsupportedRepositoryOperationException ex) { + // when versioning is not supported all nodes are considered to be + // checked out + return true; + } + } + + /** + * @see javax.jcr.Node#restore(String, boolean) + */ + @Override + public void restore(String versionName, boolean removeExisting) throws RepositoryException { + sessionDelegate.getVersionManager().restore(getPath(), versionName, removeExisting); + } + + /** + * @see javax.jcr.Node#restore(javax.jcr.version.Version, boolean) + */ + @Override + public void restore(Version version, boolean removeExisting) throws RepositoryException { + sessionDelegate.getVersionManager().restore(version, removeExisting); + } + + /** + * @see javax.jcr.Node#restore(Version, String, boolean) + */ + @Override + public void restore(Version version, String relPath, boolean removeExisting) throws RepositoryException { + // additional checks are performed with subsequent calls. + if (hasNode(relPath)) { + // node at 'relPath' exists -> call restore on the target Node + getNode(relPath).restore(version, removeExisting); + } else { + // TODO + } + } + + /** + * @see javax.jcr.Node#restoreByLabel(String, boolean) + */ + @Override + public void restoreByLabel(String versionLabel, boolean removeExisting) throws RepositoryException { + sessionDelegate.getVersionManager().restoreByLabel(getPath(), versionLabel, removeExisting); + } + + /** + * @see javax.jcr.Node#getVersionHistory() + */ + @Override + @Nonnull + public VersionHistory getVersionHistory() throws RepositoryException { + return sessionDelegate.getVersionManager().getVersionHistory(getPath()); + } + + /** + * @see javax.jcr.Node#getBaseVersion() + */ + @Override + @Nonnull + public Version getBaseVersion() throws RepositoryException { + return sessionDelegate.getVersionManager().getBaseVersion(getPath()); + } + + /** + * Checks whether this node is locked by looking for the + * {@code jcr:lockOwner} property either on this node or + * on any ancestor that also has the {@code jcr:lockIsDeep} + * property set to {@code true}. + */ + @Override + public boolean isLocked() throws RepositoryException { + String lockOwner = sessionDelegate.getOakPathOrThrow(JCR_LOCK_OWNER); + String lockIsDeep = sessionDelegate.getOakPathOrThrow(JCR_LOCK_IS_DEEP); + + if (dlg.getProperty(lockOwner) != null) { + return true; + } + + NodeDelegate parent = dlg.getParent(); + while (parent != null) { + if (parent.getProperty(lockOwner) != null) { + PropertyDelegate isDeep = parent.getProperty(lockIsDeep); + if (isDeep != null && !isDeep.isMultivalue() + && isDeep.getValue().getBoolean()) { + return true; + } + } + parent = parent.getParent(); + } + + return false; + } + + /** + * Checks whether this node holds a lock by looking for the + * {@code jcr:lockOwner} property. + */ + @Override + public boolean holdsLock() throws RepositoryException { + String lockOwner = sessionDelegate.getOakPathOrThrow(JCR_LOCK_OWNER); + return dlg.getProperty(lockOwner) != null; + } + + /** + * @see javax.jcr.Node#getLock() + */ + @Override + @Nonnull + public Lock getLock() throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @see javax.jcr.Node#lock(boolean, boolean) + */ + @Override + @Nonnull + public Lock lock(final boolean isDeep, boolean isSessionScoped) + throws RepositoryException { + final String userID = getSession().getUserID(); + + String lockOwner = sessionDelegate.getOakPathOrThrow(JCR_LOCK_OWNER); + String lockIsDeep = sessionDelegate.getOakPathOrThrow(JCR_LOCK_IS_DEEP); + try { + ContentSession session = sessionDelegate.getContentSession(); + Root root = session.getLatestRoot(); + Tree tree = root.getTree(dlg.getPath()); + if (tree == null) { + throw new ItemNotFoundException(); + } + tree.setProperty(lockOwner, userID); + tree.setProperty(lockIsDeep, isDeep); + root.commit(); // TODO: fail instead? + } catch (CommitFailedException e) { + throw new RepositoryException("Unable to lock " + this, e); + } + + getSession().refresh(true); + + if (isSessionScoped) { + return TODO.dummyImplementation().returnValue(new Lock() { + @Override + public String getLockOwner() { + return userID; + } + + @Override + public boolean isDeep() { + return isDeep; + } + + @Override + public Node getNode() { + return NodeImpl.this; + } + + @Override + public String getLockToken() { + return null; + } + + @Override + public long getSecondsRemaining() { + return Long.MAX_VALUE; + } + + @Override + public boolean isLive() { + return true; + } + + @Override + public boolean isSessionScoped() { + return true; + } + + @Override + public boolean isLockOwningSession() { + return true; + } + + @Override + public void refresh() { + } + }); + } + + return getLock(); + } + + /** + * @see javax.jcr.Node#unlock() + */ + @Override + public void unlock() throws RepositoryException { + String lockOwner = sessionDelegate.getOakPathOrThrow(JCR_LOCK_OWNER); + String lockIsDeep = sessionDelegate.getOakPathOrThrow(JCR_LOCK_IS_DEEP); + try { + Root root = sessionDelegate.getContentSession().getLatestRoot(); + Tree tree = root.getTree(dlg.getPath()); + if (tree == null) { + throw new ItemNotFoundException(); + } + tree.removeProperty(lockOwner); + tree.removeProperty(lockIsDeep); + root.commit(); + } catch (CommitFailedException e) { + throw new RepositoryException("Unable to unlock " + this, e); + } + + getSession().refresh(true); + } + + @Override + @Nonnull + public NodeIterator getSharedSet() throws RepositoryException { + throw new UnsupportedRepositoryOperationException("TODO: Node.getSharedSet"); + } + + @Override + public void removeSharedSet() throws RepositoryException { + throw new UnsupportedRepositoryOperationException("TODO: Node.removeSharedSet"); + } + + @Override + public void removeShare() throws RepositoryException { + throw new UnsupportedRepositoryOperationException("TODO: Node.removeShare"); + } + + /** + * @see javax.jcr.Node#followLifecycleTransition(String) + */ + @Override + public void followLifecycleTransition(String transition) throws RepositoryException { + throw new UnsupportedRepositoryOperationException("Lifecycle Management is not supported"); + } + + /** + * @see javax.jcr.Node#getAllowedLifecycleTransistions() + */ + @Override + @Nonnull + public String[] getAllowedLifecycleTransistions() throws RepositoryException { + throw new UnsupportedRepositoryOperationException("Lifecycle Management is not supported"); + + } + + //------------------------------------------------------------< private >--- + + private static Iterator nodeIterator(Iterator childNodes) { + return Iterators.transform( + childNodes, + new Function() { + @Override + public Node apply(NodeDelegate nodeDelegate) { + return new NodeImpl(nodeDelegate); + } + }); + } + + private static Iterator propertyIterator(Iterator properties) { + return Iterators.transform( + properties, + new Function() { + @Override + public Property apply(PropertyDelegate propertyDelegate) { + return new PropertyImpl(propertyDelegate); + } + }); + } + + private static int getTargetType(Value value, PropertyDefinition definition) { + if (definition.getRequiredType() == PropertyType.UNDEFINED) { + return value.getType(); + } else { + return definition.getRequiredType(); + } + } + + private static int getTargetType(Value[] values, PropertyDefinition definition) { + if (definition.getRequiredType() == PropertyType.UNDEFINED) { + if (values.length != 0) { + for (Value v : values) { + if (v != null) { + return v.getType(); + } + } + } + return PropertyType.STRING; + } else { + return definition.getRequiredType(); + } + } + + private void autoCreateItems() throws RepositoryException { + Iterable types = dlg.sessionDelegate.getEffectiveNodeTypeProvider().getEffectiveNodeTypes(this); + for (NodeType nt : types) { + for (PropertyDefinition pd : nt.getPropertyDefinitions()) { + if (pd.isAutoCreated() && dlg.getProperty(pd.getName()) == null) { + if (pd.isMultiple()) { + dlg.setProperty(pd.getName(), getAutoCreatedValues(pd)); + } else { + dlg.setProperty(pd.getName(), getAutoCreatedValue(pd)); + } + } + } + } + for (NodeType nt : types) { + for (NodeDefinition nd : nt.getChildNodeDefinitions()) { + if (nd.isAutoCreated() && dlg.getChild(nd.getName()) == null) { + autoCreateNode(nd); + } + } + } + } + + private Value getAutoCreatedValue(PropertyDefinition definition) + throws RepositoryException { + String name = definition.getName(); + String declaringNT = definition.getDeclaringNodeType().getName(); + + if (NodeTypeConstants.JCR_UUID.equals(name)) { + // jcr:uuid property of the mix:referenceable node type + if (NodeTypeConstants.MIX_REFERENCEABLE.equals(declaringNT)) { + return getValueFactory().createValue(IdentifierManager.generateUUID()); + } + } else if (NodeTypeConstants.JCR_CREATED.equals(name)) { + // jcr:created property of a version or a mix:created + if (NodeTypeConstants.MIX_CREATED.equals(declaringNT) + || NodeTypeConstants.NT_VERSION.equals(declaringNT)) { + return getValueFactory().createValue(Calendar.getInstance()); + } + } else if (NodeTypeConstants.JCR_CREATEDBY.equals(name)) { + // jcr:createdBy property of a mix:created + if (NodeTypeConstants.MIX_CREATED.equals(declaringNT)) { + return getValueFactory().createValue( + sessionDelegate.getAuthInfo().getUserID()); + } + } else if (NodeTypeConstants.JCR_LASTMODIFIED.equals(name)) { + // jcr:lastModified property of a mix:lastModified + if (NodeTypeConstants.MIX_LASTMODIFIED.equals(declaringNT)) { + return getValueFactory().createValue(Calendar.getInstance()); + } + } else if (NodeTypeConstants.JCR_LASTMODIFIEDBY.equals(name)) { + // jcr:lastModifiedBy property of a mix:lastModified + if (NodeTypeConstants.MIX_LASTMODIFIED.equals(declaringNT)) { + return getValueFactory().createValue( + sessionDelegate.getAuthInfo().getUserID()); + } + } + // does the definition have a default value? + if (definition.getDefaultValues() != null) { + Value[] values = definition.getDefaultValues(); + if (values.length > 0) { + return values[0]; + } + } + throw new RepositoryException("Unable to auto-create value for " + + PathUtils.concat(getPath(), name)); + } + + private Iterable getAutoCreatedValues(PropertyDefinition definition) + throws RepositoryException { + String name = definition.getName(); + + // default values? + if (definition.getDefaultValues() != null) { + return Lists.newArrayList(definition.getDefaultValues()); + } + throw new RepositoryException("Unable to auto-create value for " + + PathUtils.concat(getPath(), name)); + } + + private void autoCreateNode(NodeDefinition definition) + throws RepositoryException { + addNode(definition.getName(), definition.getDefaultPrimaryTypeName()); + } + + // FIXME: hack to filter for a subset of supported mixins for now + // this allows only harmless mixin types so that other code like addMixin gets test coverage + private boolean isSupportedMixinName(String mixinName) throws RepositoryException { + String oakName = sessionDelegate.getOakPathOrThrow(mixinName); + return "mix:title".equals(oakName) || + NodeTypeConstants.MIX_REFERENCEABLE.equals(oakName) || + NodeTypeConstants.MIX_VERSIONABLE.equals(oakName) || + NodeTypeConstants.MIX_LOCKABLE.equals(oakName); + } + + private void checkValidWorkspace(String workspaceName) throws RepositoryException { + for (String wn : sessionDelegate.getWorkspace().getAccessibleWorkspaceNames()) { + if (wn.equals(workspaceName)) { + return; + } + } + throw new NoSuchWorkspaceException(workspaceName + " does not exist."); + } + + private void internalSetPrimaryType(final String nodeTypeName) throws RepositoryException { + sessionDelegate.perform(new SessionOperation() { + @Override + public Void perform() throws RepositoryException { + // TODO: figure out the right place for this check + NodeTypeManager ntm = sessionDelegate.getNodeTypeManager(); + NodeType nt = ntm.getNodeType(nodeTypeName); // throws on not found + if (nt.isAbstract() || nt.isMixin()) { + throw new ConstraintViolationException(); + } + // TODO: END + + String jcrPrimaryType = sessionDelegate.getOakPathOrThrow(Property.JCR_PRIMARY_TYPE); + Value value = sessionDelegate.getValueFactory().createValue(nodeTypeName, PropertyType.NAME); + dlg.setProperty(jcrPrimaryType, value); + return null; + } + }); + } + + private Property internalSetProperty(final String jcrName, final Value value, + final int type, final boolean exactTypeMatch) throws RepositoryException { + checkStatus(); + checkProtected(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public Property perform() throws RepositoryException { + String oakName = sessionDelegate.getOakPathOrThrow(jcrName); + if (value == null) { + if (hasProperty(jcrName)) { + Property property = getProperty(jcrName); + property.remove(); + return property; + } else { + // Return a property instance which throws on access. See OAK-395 + return new PropertyImpl(new PropertyDelegate( + sessionDelegate, dlg.getLocation().getChild(oakName))); + } + } else { + PropertyDefinition definition; + if (hasProperty(jcrName)) { + definition = getProperty(jcrName).getDefinition(); + } else { + definition = dlg.sessionDelegate.getDefinitionProvider().getDefinition(NodeImpl.this, oakName, false, type, exactTypeMatch); + } + checkProtected(definition); + if (definition.isMultiple()) { + throw new ValueFormatException("Cannot set single value to multivalued property"); + } + + int targetType = getTargetType(value, definition); + Value targetValue = ValueHelper.convert(value, targetType, getValueFactory()); + + return new PropertyImpl(dlg.setProperty(oakName, targetValue)); + } + } + }); + } + + private Property internalSetProperty(final String jcrName, final Value[] values, + final int type, final boolean exactTypeMatch) throws RepositoryException { + checkStatus(); + checkProtected(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public Property perform() throws RepositoryException { + String oakName = sessionDelegate.getOakPathOrThrow(jcrName); + if (values == null) { + if (hasProperty(jcrName)) { + Property property = getProperty(jcrName); + property.remove(); + return property; + } else { + return new PropertyImpl(new PropertyDelegate( + sessionDelegate, dlg.getLocation().getChild(oakName))); + } + } else { + PropertyDefinition definition; + if (hasProperty(jcrName)) { + definition = getProperty(jcrName).getDefinition(); + } else { + definition = dlg.sessionDelegate.getDefinitionProvider().getDefinition(NodeImpl.this, oakName, true, type, exactTypeMatch); + } + checkProtected(definition); + if (!definition.isMultiple()) { + throw new ValueFormatException("Cannot set value array to single value property"); + } + + int targetType = getTargetType(values, definition); + Value[] targetValues = ValueHelper.convert(values, targetType, getValueFactory()); + + Iterable nonNullValues = Iterables.filter( + Arrays.asList(targetValues), + Predicates.notNull()); + return new PropertyImpl(dlg.setProperty(oakName, nonNullValues)); + } + } + }); + } +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/OakRepositoryFactory.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/OakRepositoryFactory.java new file mode 100644 index 00000000000..ac6d23eef21 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/OakRepositoryFactory.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.jcr; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Map; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; + +public class OakRepositoryFactory implements RepositoryFactory { + + private static final String REPOSITORY_URI = "org.apache.jackrabbit.repository.uri"; + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public Repository getRepository(Map parameters) throws RepositoryException { + Object value = parameters == null ? null : parameters.get(REPOSITORY_URI); + if (value != null) { + try { + URI uri = new URI(value.toString()); + if (uri.getScheme().equalsIgnoreCase("jcr-oak")) { + return getRepository(uri, parameters); + } + } catch (URISyntaxException ignore) { + } + } + return null; + } + + private static Repository getRepository( + URI uri, Map parameters) + throws RepositoryException { + // TODO correctly interpret uri + return new Jcr().createRepository(); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/PropertyDelegate.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/PropertyDelegate.java new file mode 100644 index 00000000000..9ab87a58eee --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/PropertyDelegate.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.jcr; + +import java.util.List; + +import javax.annotation.Nonnull; +import javax.jcr.InvalidItemStateException; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.TreeLocation; +import org.apache.jackrabbit.oak.core.TreeImpl.PropertyLocation; +import org.apache.jackrabbit.oak.plugins.memory.PropertyStates; +import org.apache.jackrabbit.oak.plugins.value.ValueFactoryImpl; + +/** + * {@code PropertyDelegate} serve as internal representations of {@code Property}s. + * Most methods of this class throw an {@code InvalidItemStateException} + * exception if the instance is stale. An instance is stale if the underlying + * items does not exist anymore. + */ +public class PropertyDelegate extends ItemDelegate { + + PropertyDelegate(SessionDelegate sessionDelegate, TreeLocation location) { + super(sessionDelegate, location); + } + + /** + * Get the value of the property + * @return the value of the property + * @throws InvalidItemStateException + */ + @Nonnull + public Value getValue() throws InvalidItemStateException { + return ValueFactoryImpl.createValue(getPropertyState(), sessionDelegate.getNamePathMapper()); + } + + /** + * Get the values of the property + * @return the values of the property + * @throws InvalidItemStateException + */ + @Nonnull + public List getValues() throws InvalidItemStateException { + return ValueFactoryImpl.createValues(getPropertyState(), sessionDelegate.getNamePathMapper()); + } + + /** + * Determine whether the property is multi valued + * @return {@code true} if multi valued + */ + public boolean isMultivalue() throws InvalidItemStateException { + return getPropertyState().isArray(); + } + + /** + * Set the value of the property + * @param value + */ + public void setValue(Value value) throws RepositoryException { + getPropertyLocation().set(PropertyStates.createProperty(getName(), value)); + } + + /** + * Set the values of the property + * @param values + */ + public void setValues(Iterable values) throws RepositoryException { + getPropertyLocation().set(PropertyStates.createProperty(getName(), values)); + } + + /** + * Remove the property + */ + public void remove() throws InvalidItemStateException { + getPropertyLocation().remove(); + } + + //------------------------------------------------------------< private >--- + + @Nonnull + private PropertyState getPropertyState() throws InvalidItemStateException { + return getPropertyLocation().getProperty(); // Not null + } + + private PropertyLocation getPropertyLocation() throws InvalidItemStateException { + TreeLocation location = getLocation(); + if (!(location instanceof PropertyLocation)) { + throw new InvalidItemStateException(); + } + return (PropertyLocation) location; + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/PropertyImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/PropertyImpl.java new file mode 100644 index 00000000000..45da6a09c13 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/PropertyImpl.java @@ -0,0 +1,671 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.List; + +import javax.annotation.Nonnull; +import javax.jcr.AccessDeniedException; +import javax.jcr.Binary; +import javax.jcr.ItemNotFoundException; +import javax.jcr.ItemVisitor; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.nodetype.PropertyDefinition; + +import com.google.common.base.Predicates; +import com.google.common.collect.Iterables; +import org.apache.jackrabbit.oak.api.Tree.Status; +import org.apache.jackrabbit.value.ValueHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * {@code PropertyImpl}... + */ +public class PropertyImpl extends ItemImpl implements Property { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(PropertyImpl.class); + + PropertyImpl(PropertyDelegate dlg) { + super(dlg.getSessionDelegate(), dlg); + } + + //---------------------------------------------------------------< Item >--- + + /** + * @see javax.jcr.Item#isNode() + */ + @Override + public boolean isNode() { + return false; + } + + /** + * @see javax.jcr.Item#getParent() + */ + @Override + @Nonnull + public Node getParent() throws RepositoryException { + return sessionDelegate.perform(new SessionOperation() { + @Override + public NodeImpl perform() throws RepositoryException { + NodeDelegate parent = dlg.getParent(); + if (parent == null) { + throw new AccessDeniedException(); + } else { + return new NodeImpl(dlg.getParent()); + } + } + }); + } + + /** + * @see javax.jcr.Item#isNew() + */ + @Override + public boolean isNew() { + try { + return sessionDelegate.perform(new SessionOperation() { + @Override + public Boolean perform() throws RepositoryException { + return dlg.getStatus() == Status.NEW; + } + }); + } catch (RepositoryException e) { + return false; + } + } + + /** + * @see javax.jcr.Item#isModified() () + */ + @Override + public boolean isModified() { + try { + return sessionDelegate.perform(new SessionOperation() { + @Override + public Boolean perform() throws RepositoryException { + return dlg.getStatus() == Status.MODIFIED; + } + }); + } catch (RepositoryException e) { + return false; + } + } + + /** + * @see javax.jcr.Item#isNew() + */ + @Override + public void remove() throws RepositoryException { + checkStatus(); + checkProtected(); + + sessionDelegate.perform(new SessionOperation() { + @Override + public Void perform() throws RepositoryException { + dlg.remove(); + return null; + } + }); + } + + /** + * @see javax.jcr.Item#accept(javax.jcr.ItemVisitor) + */ + @Override + public void accept(ItemVisitor visitor) throws RepositoryException { + checkStatus(); + visitor.visit(this); + } + + //-----------------------------------------------------------< Property >--- + + /** + * @see Property#setValue(Value) + */ + @Override + public void setValue(Value value) throws RepositoryException { + checkStatus(); + + int valueType = (value != null) ? value.getType() : PropertyType.UNDEFINED; + int reqType = getRequiredType(valueType); + setValue(value, reqType); + } + + /** + * @see Property#setValue(Value[]) + */ + @Override + public void setValue(final Value[] values) throws RepositoryException { + checkStatus(); + + sessionDelegate.perform(new SessionOperation() { + @Override + public Void perform() throws RepositoryException { + // assert equal types for all values entries + int valueType = PropertyType.UNDEFINED; + if (values != null) { + for (Value value : values) { + if (value == null) { + // skip null values as those will be purged later + continue; + } + if (valueType == PropertyType.UNDEFINED) { + valueType = value.getType(); + } else if (valueType != value.getType()) { + String msg = "Inhomogeneous type of values (" + this + ')'; + log.debug(msg); + throw new ValueFormatException(msg); + } + } + } + + int reqType = getRequiredType(valueType); + setValues(values, reqType); + return null; + } + }); + } + + /** + * @see Property#setValue(String) + */ + @Override + public void setValue(String value) throws RepositoryException { + checkStatus(); + + int reqType = getRequiredType(PropertyType.STRING); + if (value == null) { + setValue(null, reqType); + } else { + setValue(getValueFactory().createValue(value), reqType); + } + } + + /** + * @see Property#setValue(String[]) + */ + @Override + public void setValue(String[] values) throws RepositoryException { + checkStatus(); + + int reqType = getRequiredType(PropertyType.STRING); + if (values == null) { + setValues(null, reqType); + } else { + List vs = new ArrayList(values.length); + for (String value : values) { + if (value != null) { + vs.add(getValueFactory().createValue(value)); + } + } + setValues(vs.toArray(new Value[vs.size()]), reqType); + } + } + + /** + * @see Property#setValue(InputStream) + */ + @SuppressWarnings("deprecation") + @Override + public void setValue(InputStream value) throws RepositoryException { + checkStatus(); + + int reqType = getRequiredType(PropertyType.BINARY); + if (value == null) { + setValue(null, reqType); + } else { + setValue(getValueFactory().createValue(value), reqType); + } + } + + /** + * @see Property#setValue(Binary) + */ + @Override + public void setValue(Binary value) throws RepositoryException { + checkStatus(); + + int reqType = getRequiredType(PropertyType.BINARY); + if (value == null) { + setValue(null, reqType); + } else { + setValue(getValueFactory().createValue(value), reqType); + } + } + + /** + * @see Property#setValue(long) + */ + @Override + public void setValue(long value) throws RepositoryException { + checkStatus(); + + int reqType = getRequiredType(PropertyType.LONG); + setValue(getValueFactory().createValue(value), reqType); + } + + /** + * @see Property#setValue(double) + */ + @Override + public void setValue(double value) throws RepositoryException { + checkStatus(); + + int reqType = getRequiredType(PropertyType.DOUBLE); + setValue(getValueFactory().createValue(value), reqType); + } + + /** + * @see Property#setValue(BigDecimal) + */ + @Override + public void setValue(BigDecimal value) throws RepositoryException { + checkStatus(); + + int reqType = getRequiredType(PropertyType.DECIMAL); + if (value == null) { + setValue(null, reqType); + } else { + setValue(getValueFactory().createValue(value), reqType); + } + } + + /** + * @see Property#setValue(Calendar) + */ + @Override + public void setValue(Calendar value) throws RepositoryException { + checkStatus(); + + int reqType = getRequiredType(PropertyType.DATE); + if (value == null) { + setValue(null, reqType); + } else { + setValue(getValueFactory().createValue(value), reqType); + } + } + + /** + * @see Property#setValue(boolean) + */ + @Override + public void setValue(boolean value) throws RepositoryException { + checkStatus(); + + int reqType = getRequiredType(PropertyType.BOOLEAN); + setValue(getValueFactory().createValue(value), reqType); + } + + /** + * @see Property#setValue(javax.jcr.Node) + */ + @Override + public void setValue(Node value) throws RepositoryException { + checkStatus(); + + int reqType = getRequiredType(PropertyType.REFERENCE); + if (value == null) { + setValue(null, reqType); + } else { + setValue(getValueFactory().createValue(value), reqType); + } + } + + @Override + @Nonnull + public Value getValue() throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public Value perform() throws RepositoryException { + if (isMultiple()) { + throw new ValueFormatException(this + " is multi-valued."); + } + + return dlg.getValue(); + } + }); + } + + @Override + @Nonnull + public Value[] getValues() throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public Value[] perform() throws RepositoryException { + if (!isMultiple()) { + throw new ValueFormatException(this + " is not multi-valued."); + } + + return Iterables.toArray(dlg.getValues(), Value.class); + } + }); + } + + /** + * @see Property#getString() + */ + @Override + @Nonnull + public String getString() throws RepositoryException { + return getValue().getString(); + } + + /** + * @see Property#getStream() + */ + @SuppressWarnings("deprecation") + @Override + @Nonnull + public InputStream getStream() throws RepositoryException { + return getValue().getStream(); + } + + /** + * @see javax.jcr.Property#getBinary() + */ + @Override + @Nonnull + public Binary getBinary() throws RepositoryException { + return getValue().getBinary(); + } + + /** + * @see Property#getLong() + */ + @Override + public long getLong() throws RepositoryException { + return getValue().getLong(); + } + + /** + * @see Property#getDouble() + */ + @Override + public double getDouble() throws RepositoryException { + return getValue().getDouble(); + } + + /** + * @see Property#getDecimal() + */ + @Override + @Nonnull + public BigDecimal getDecimal() throws RepositoryException { + return getValue().getDecimal(); + } + + /** + * @see Property#getDate() + */ + @Override + @Nonnull + public Calendar getDate() throws RepositoryException { + return getValue().getDate(); + } + + /** + * @see Property#getBoolean() + */ + @Override + public boolean getBoolean() throws RepositoryException { + return getValue().getBoolean(); + } + + /** + * @see javax.jcr.Property#getNode() + */ + @Override + @Nonnull + public Node getNode() throws RepositoryException { + return sessionDelegate.perform(new SessionOperation() { + @Override + public Node perform() throws RepositoryException { + Value value = getValue(); + switch (value.getType()) { + case PropertyType.REFERENCE: + case PropertyType.WEAKREFERENCE: + return getSession().getNodeByIdentifier(value.getString()); + + case PropertyType.PATH: + case PropertyType.NAME: + String path = value.getString(); + if (path.startsWith("[") && path.endsWith("]")) { + // identifier path + String identifier = path.substring(1, path.length() - 1); + return getSession().getNodeByIdentifier(identifier); + } + else { + try { + return (path.charAt(0) == '/') ? getSession().getNode(path) : getParent().getNode(path); + } catch (PathNotFoundException e) { + throw new ItemNotFoundException(path); + } + } + + case PropertyType.STRING: + try { + Value refValue = ValueHelper.convert(value, PropertyType.REFERENCE, getValueFactory()); + return getSession().getNodeByIdentifier(refValue.getString()); + } catch (ItemNotFoundException e) { + throw e; + } catch (RepositoryException e) { + // try if STRING value can be interpreted as PATH value + Value pathValue = ValueHelper.convert(value, PropertyType.PATH, getValueFactory()); + path = pathValue.getString(); + try { + return (path.charAt(0) == '/') ? getSession().getNode(path) : getParent().getNode(path); + } catch (PathNotFoundException e1) { + throw new ItemNotFoundException(pathValue.getString()); + } + } + + default: + throw new ValueFormatException("Property value cannot be converted to a PATH, REFERENCE or WEAKREFERENCE"); + } + } + }); + } + + /** + * @see javax.jcr.Property#getProperty() + */ + @Override + @Nonnull + public Property getProperty() throws RepositoryException { + return sessionDelegate.perform(new SessionOperation() { + @Override + public Property perform() throws RepositoryException { + Value value = getValue(); + Value pathValue = ValueHelper.convert(value, PropertyType.PATH, getValueFactory()); + String path = pathValue.getString(); + try { + return (path.charAt(0) == '/') ? getSession().getProperty(path) : getParent().getProperty(path); + } catch (PathNotFoundException e) { + throw new ItemNotFoundException(path); + } + } + }); + } + + /** + * @see javax.jcr.Property#getLength() + */ + @Override + public long getLength() throws RepositoryException { + return getLength(getValue()); + } + + /** + * @see javax.jcr.Property#getLengths() + */ + @Override + @Nonnull + public long[] getLengths() throws RepositoryException { + Value[] values = getValues(); + long[] lengths = new long[values.length]; + + for (int i = 0; i < values.length; i++) { + lengths[i] = getLength(values[i]); + } + return lengths; + } + + @Override + @Nonnull + public PropertyDefinition getDefinition() throws RepositoryException { + return dlg.sessionDelegate.getDefinitionProvider().getDefinition(getParent(), this); + } + + /** + * @see javax.jcr.Property#getType() + */ + @Override + public int getType() throws RepositoryException { + return sessionDelegate.perform(new SessionOperation() { + @Override + public Integer perform() throws RepositoryException { + if (isMultiple()) { + Value[] values = getValues(); + if (values.length == 0) { + // retrieve the type from the property definition + return getRequiredType(PropertyType.UNDEFINED); + } else { + return values[0].getType(); + } + } else { + return getValue().getType(); + } + } + }); + } + + @Override + public boolean isMultiple() throws RepositoryException { + checkStatus(); + + return sessionDelegate.perform(new SessionOperation() { + @Override + public Boolean perform() throws RepositoryException { + return dlg.isMultivalue(); + } + }); + } + + //------------------------------------------------------------< private >--- + + /** + * Return the length of the specified JCR value object. + * + * @param value The value. + * @return The length of the given value. + * @throws RepositoryException If an error occurs. + */ + private static long getLength(Value value) throws RepositoryException { + if (value.getType() == PropertyType.BINARY) { + return value.getBinary().getSize(); + } else { + return value.getString().length(); + } + } + + /** + * @param defaultType + * @return the required type for this property. + * @throws javax.jcr.RepositoryException + */ + private int getRequiredType(int defaultType) throws RepositoryException { + // check type according to definition of this property + int reqType = getDefinition().getRequiredType(); + if (reqType == PropertyType.UNDEFINED) { + if (defaultType == PropertyType.UNDEFINED) { + reqType = PropertyType.STRING; + } else { + reqType = defaultType; + } + } + return reqType; + } + + /** + * @param value + * @param requiredType + * @throws RepositoryException + */ + private void setValue(Value value, int requiredType) throws RepositoryException { + checkArgument(requiredType != PropertyType.UNDEFINED); + checkProtected(); + + // TODO check again if definition validation should be respected here. + if (isMultiple()) { + throw new ValueFormatException("Attempt to set a single value to multi-valued property."); + } + if (value == null) { + dlg.remove(); + } else { + Value targetValue = ValueHelper.convert(value, requiredType, sessionDelegate.getValueFactory()); + dlg.setValue(targetValue); + } + } + + /** + * @param values + * @param requiredType + * @throws RepositoryException + */ + private void setValues(Value[] values, int requiredType) throws RepositoryException { + checkArgument(requiredType != PropertyType.UNDEFINED); + checkProtected(); + + // TODO check again if definition validation should be respected here. + if (!isMultiple()) { + throw new ValueFormatException("Attempt to set multiple values to single valued property."); + } + if (values == null) { + dlg.remove(); + } else { + Value[] targetValues = ValueHelper.convert(values, requiredType, sessionDelegate.getValueFactory()); + Iterable nonNullValues = Iterables.filter( + Arrays.asList(targetValues), + Predicates.notNull()); + + dlg.setValues(nonNullValues); + } + } + +} \ No newline at end of file diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/RepositoryImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/RepositoryImpl.java new file mode 100644 index 00000000000..5aa08d58c4e --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/RepositoryImpl.java @@ -0,0 +1,170 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr; + +import java.util.concurrent.ScheduledExecutorService; + +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.commons.SimpleValueFactory; +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@code RepositoryImpl}... + */ +public class RepositoryImpl implements Repository { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(RepositoryImpl.class); + + private final Descriptors descriptors = new Descriptors(new SimpleValueFactory()); + private final ContentRepository contentRepository; + + private final ScheduledExecutorService executor; + + private final SecurityProvider securityProvider; + + public RepositoryImpl( + ContentRepository contentRepository, + ScheduledExecutorService executor, + SecurityProvider securityProvider) { + this.contentRepository = contentRepository; + this.executor = executor; + this.securityProvider = securityProvider; + } + + //---------------------------------------------------------< Repository >--- + /** + * @see javax.jcr.Repository#getDescriptorKeys() + */ + @Override + public String[] getDescriptorKeys() { + return descriptors.getKeys(); + } + + /** + * @see Repository#isStandardDescriptor(String) + */ + @Override + public boolean isStandardDescriptor(String key) { + return descriptors.isStandardDescriptor(key); + } + + /** + * @see javax.jcr.Repository#getDescriptor(String) + */ + @Override + public String getDescriptor(String key) { + try { + Value v = getDescriptorValue(key); + return v == null + ? null + : v.getString(); + } catch (RepositoryException e) { + log.debug("Error converting value for descriptor with key {} to string", key); + return null; + } + } + + /** + * @see javax.jcr.Repository#getDescriptorValue(String) + */ + @Override + public Value getDescriptorValue(String key) { + return descriptors.getValue(key); + } + + /** + * @see javax.jcr.Repository#getDescriptorValues(String) + */ + @Override + public Value[] getDescriptorValues(String key) { + return descriptors.getValues(key); + } + + /** + * @see javax.jcr.Repository#isSingleValueDescriptor(String) + */ + @Override + public boolean isSingleValueDescriptor(String key) { + return descriptors.isSingleValueDescriptor(key); + } + + /** + * @see javax.jcr.Repository#login(javax.jcr.Credentials, String) + */ + @Override + public Session login(Credentials credentials, String workspaceName) throws RepositoryException { + // TODO: needs complete refactoring + try { + ContentSession contentSession = contentRepository.login(credentials, workspaceName); + return new SessionDelegate(this, executor, contentSession, securityProvider, false).getSession(); + } catch (LoginException e) { + throw new javax.jcr.LoginException(e.getMessage(), e); + } + } + + /** + * Calls {@link Repository#login(Credentials, String)} with + * {@code null} arguments. + * + * @return logged in session + * @throws RepositoryException if an error occurs + */ + @Override + public Session login() throws RepositoryException { + return login(null, null); + } + + /** + * Calls {@link Repository#login(Credentials, String)} with + * the given credentials and a {@code null} workspace name. + * + * @param credentials login credentials + * @return logged in session + * @throws RepositoryException if an error occurs + */ + @Override + public Session login(Credentials credentials) throws RepositoryException { + return login(credentials, null); + } + + /** + * Calls {@link Repository#login(Credentials, String)} with + * {@code null} credentials and the given workspace name. + * + * @param workspace workspace name + * @return logged in session + * @throws RepositoryException if an error occurs + */ + @Override + public Session login(String workspace) throws RepositoryException { + return login(null, workspace); + } + +} \ No newline at end of file diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/SessionDelegate.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/SessionDelegate.java new file mode 100644 index 00000000000..198883e66f0 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/SessionDelegate.java @@ -0,0 +1,527 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr; + +import java.io.IOException; +import java.util.concurrent.ScheduledExecutorService; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.jcr.ItemExistsException; +import javax.jcr.PathNotFoundException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Workspace; +import javax.jcr.lock.LockManager; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.observation.ObservationManager; +import javax.jcr.query.QueryManager; +import javax.jcr.version.VersionManager; + +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.api.AuthInfo; +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.SessionQueryEngine; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.api.TreeLocation; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.namepath.NamePathMapperImpl; +import org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager; +import org.apache.jackrabbit.oak.plugins.nodetype.DefinitionProvider; +import org.apache.jackrabbit.oak.plugins.nodetype.EffectiveNodeTypeProvider; +import org.apache.jackrabbit.oak.plugins.observation.ObservationManagerImpl; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeManagerImpl; +import org.apache.jackrabbit.oak.plugins.value.ValueFactoryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class SessionDelegate { + static final Logger log = LoggerFactory.getLogger(SessionDelegate.class); + + private final NamePathMapper namePathMapper; + private final Repository repository; + private final ScheduledExecutorService executor; + private final ContentSession contentSession; + private final ValueFactoryImpl valueFactory; + private final Workspace workspace; + private final Session session; + private final Root root; + private final boolean autoRefresh; + + private final SecurityProvider securityProvider; + + private final IdentifierManager idManager; + + private ObservationManagerImpl observationManager; + private PrincipalManager principalManager; + private UserManager userManager; + private PrivilegeManager privilegeManager; + private boolean isAlive = true; + private int sessionOpCount; + + SessionDelegate( + Repository repository, ScheduledExecutorService executor, + ContentSession contentSession, SecurityProvider securityProvider, + boolean autoRefresh) + throws RepositoryException { + + this.repository = checkNotNull(repository); + this.executor = executor; + this.contentSession = checkNotNull(contentSession); + this.securityProvider = securityProvider; + this.autoRefresh = autoRefresh; + + this.root = contentSession.getLatestRoot(); + this.workspace = new WorkspaceImpl(this); + this.session = new SessionImpl(this); + this.idManager = new IdentifierManager(root); + this.namePathMapper = new NamePathMapperImpl(new SessionNameMapper(this), idManager); + this.valueFactory = new ValueFactoryImpl(root.getBlobFactory(), namePathMapper); + } + + /** + * Performs the passed {@code SessionOperation} in a safe execution context. This + * context ensures that the session is refreshed if necessary and that refreshing + * occurs before the session operation is performed and the refreshing is done only + * once. + * + * @param sessionOperation the {@code SessionOperation} to perform + * @param return type of {@code sessionOperation} + * @return the result of {@code sessionOperation.perform()} + * @throws RepositoryException + */ + public T perform(SessionOperation sessionOperation) throws RepositoryException { + try { + sessionOpCount++; + if (needsRefresh()) { + refresh(true); + } + return sessionOperation.perform(); + } finally { + sessionOpCount--; + } + } + + private boolean needsRefresh() { + // Refresh is always needed if this is an auto refresh session. Otherwise + // refresh in only needed for non re-entrant session operations and only if + // observation events have actually been delivered + return autoRefresh || + (sessionOpCount <= 1 && observationManager != null && observationManager.hasEvents()); + } + + public boolean isAlive() { + return isAlive; + } + + @Nonnull + public Session getSession() { + return session; + } + + @Nonnull + public AuthInfo getAuthInfo() { + return contentSession.getAuthInfo(); + } + + @Nonnull + public Repository getRepository() { + return repository; + } + + public void logout() { + if (!isAlive) { + // ignore + return; + } + + isAlive = false; + if (observationManager != null) { + observationManager.dispose(); + } + // TODO + + try { + contentSession.close(); + } catch (IOException e) { + log.warn("Error while closing connection", e); + } + } + + @CheckForNull + public NodeDelegate getRootNode() { + return getNode("/"); + } + + @CheckForNull + public Root getRoot() { + return root; + } + + /** + * {@code NodeDelegate} at the given path + * @param path Oak path + * @return The {@code NodeDelegate} at {@code path} or {@code null} if + * none exists or not accessible. + */ + @CheckForNull + public NodeDelegate getNode(String path) { + return NodeDelegate.create(this, getLocation(path)); + } + + @CheckForNull + public NodeDelegate getNodeByIdentifier(String id) { + Tree tree = idManager.getTree(id); + return (tree == null) ? null : new NodeDelegate(this, tree); + } + + @CheckForNull + /** + * {@code PropertyDelegate} at the given path + * @param path Oak path + * @return The {@code PropertyDelegate} at {@code path} or {@code null} if + * none exists or not accessible. + */ + public PropertyDelegate getProperty(String path) { + TreeLocation location = root.getLocation(path); + return location.getProperty() == null + ? null + : new PropertyDelegate(this, location); + } + + @Nonnull + public ValueFactoryImpl getValueFactory() { + return valueFactory; + } + + @Nonnull + public NamePathMapper getNamePathMapper() { + return namePathMapper; + } + + public boolean hasPendingChanges() { + return root.hasPendingChanges(); + } + + public void save() throws RepositoryException { + try { + root.commit(); + } catch (CommitFailedException e) { + e.throwRepositoryException(); + } + } + + public void refresh(boolean keepChanges) { + if (keepChanges) { + root.rebase(); + } else { + root.refresh(); + } + // TODO: improve + if (privilegeManager != null && privilegeManager instanceof PrivilegeManagerImpl) { + ((PrivilegeManagerImpl) privilegeManager).refresh(); + } + } + + /** + * Returns the Oak name for the given JCR name, or throws a + * {@link RepositoryException} if the name is invalid or can + * otherwise not be mapped. + * + * @param jcrName JCR name + * @return Oak name + * @throws RepositoryException if the name is invalid + */ + @Nonnull + public String getOakNameOrThrow(String jcrName) throws RepositoryException { + String oakName = getNamePathMapper().getOakName(jcrName); + if (oakName != null) { + return oakName; + } else { + throw new RepositoryException("Invalid name: " + jcrName); + } + } + + /** + * Shortcut for {@code SessionDelegate.getNamePathMapper().getOakPath(jcrPath)}. + * + * @param jcrPath JCR path + * @return Oak path, or {@code null} + */ + @CheckForNull + public String getOakPathOrNull(String jcrPath) { + return getNamePathMapper().getOakPath(jcrPath); + } + + /** + * Shortcut for {@code SessionDelegate.getOakPathKeepIndex(jcrPath)}. + * + * @param jcrPath JCR path + * @return Oak path, or {@code null}, with indexes left intact + * @throws PathNotFoundException + */ + @Nonnull + public String getOakPathKeepIndexOrThrowNotFound(String jcrPath) throws PathNotFoundException { + String oakPath = getNamePathMapper().getOakPathKeepIndex(jcrPath); + if (oakPath != null) { + return oakPath; + } else { + throw new PathNotFoundException(jcrPath); + } + } + + /** + * Returns the Oak path for the given JCR path, or throws a + * {@link PathNotFoundException} if the path can not be mapped. + * + * @param jcrPath JCR path + * @return Oak path + * @throws PathNotFoundException if the path can not be mapped + */ + @Nonnull + public String getOakPathOrThrowNotFound(String jcrPath) throws PathNotFoundException { + String oakPath = getOakPathOrNull(jcrPath); + if (oakPath != null) { + return oakPath; + } else { + throw new PathNotFoundException(jcrPath); + } + } + + /** + * Returns the Oak path for the given JCR path, or throws a + * {@link RepositoryException} if the path can not be mapped. + * + * @param jcrPath JCR path + * @return Oak path + * @throws RepositoryException if the path can not be mapped + */ + @Nonnull + public String getOakPathOrThrow(String jcrPath) + throws RepositoryException { + String oakPath = getOakPathOrNull(jcrPath); + if (oakPath != null) { + return oakPath; + } else { + throw new RepositoryException("Invalid name or path: " + jcrPath); + } + } + + //----------------------------------------------------------< Workspace >--- + + @Nonnull + public Workspace getWorkspace() { + return workspace; + } + + @Nonnull + public String getWorkspaceName() { + return contentSession.getWorkspaceName(); + } + + /** + * Copy a node + * @param srcPath oak path to the source node to copy + * @param destPath oak path to the destination + * @throws RepositoryException + */ + public void copy(String srcPath, String destPath) throws RepositoryException { + // check destination + Tree dest = getTree(destPath); + if (dest != null) { + throw new ItemExistsException(destPath); + } + + // check parent of destination + String destParentPath = PathUtils.getParentPath(destPath); + Tree destParent = getTree(destParentPath); + if (destParent == null) { + throw new PathNotFoundException(PathUtils.getParentPath(destPath)); + } + + // check source exists + Tree src = getTree(srcPath); + if (src == null) { + throw new PathNotFoundException(srcPath); + } + + try { + Root currentRoot = contentSession.getLatestRoot(); + currentRoot.copy(srcPath, destPath); + currentRoot.commit(); + } + catch (CommitFailedException e) { + e.throwRepositoryException(); + } + } + + /** + * Move a node + * @param srcPath oak path to the source node to copy + * @param destPath oak path to the destination + * @param transientOp whether or not to perform the move in transient space + * @throws RepositoryException + */ + public void move(String srcPath, String destPath, boolean transientOp) + throws RepositoryException { + + Root moveRoot = transientOp ? root : contentSession.getLatestRoot(); + + // check destination + Tree dest = moveRoot.getTree(destPath); + if (dest != null) { + throw new ItemExistsException(destPath); + } + + // check parent of destination + String destParentPath = PathUtils.getParentPath(destPath); + Tree destParent = moveRoot.getTree(destParentPath); + if (destParent == null) { + throw new PathNotFoundException(PathUtils.getParentPath(destPath)); + } + + // check source exists + Tree src = moveRoot.getTree(srcPath); + if (src == null) { + throw new PathNotFoundException(srcPath); + } + + try { + moveRoot.move(srcPath, destPath); + if (!transientOp) { + moveRoot.commit(); + } + } catch (CommitFailedException e) { + e.throwRepositoryException(); + } + } + + @Nonnull + public LockManager getLockManager() throws RepositoryException { + return workspace.getLockManager(); + } + + @Nonnull + public SessionQueryEngine getQueryEngine() { + return root.getQueryEngine(); + } + + @Nonnull + public QueryManager getQueryManager() throws RepositoryException { + return workspace.getQueryManager(); + } + + @Nonnull + public NodeTypeManager getNodeTypeManager() throws RepositoryException { + return workspace.getNodeTypeManager(); + } + + @Nonnull + public VersionManager getVersionManager() throws RepositoryException { + return workspace.getVersionManager(); + } + + @Nonnull + public ObservationManager getObservationManager() { + if (observationManager == null) { + observationManager = new ObservationManagerImpl(getRoot(), getNamePathMapper(), executor); + } + return observationManager; + } + + public IdentifierManager getIdManager() { + return idManager; + } + + @Nonnull + public ContentSession getContentSession() { + return contentSession; + } + + //-----------------------------------------------------------< internal >--- + + /** + * Get the {@code Tree} with the given path + * @param path oak path + * @return tree at the given path or {@code null} if no such tree exists or + * if the tree at {@code path} is not accessible. + */ + @CheckForNull + Tree getTree(String path) { + return root.getTree(path); + } + + @Nonnull + TreeLocation getLocation(String path) { + return root.getLocation(path); + } + + @Nonnull + PrincipalManager getPrincipalManager() throws RepositoryException { + if (principalManager == null) { + if (securityProvider != null) { + principalManager = securityProvider.getPrincipalConfiguration().getPrincipalManager(session, root, getNamePathMapper()); + } else { + throw new UnsupportedRepositoryOperationException("Principal management not supported."); + } + } + return principalManager; + } + + @Nonnull + UserManager getUserManager() throws UnsupportedRepositoryOperationException { + if (userManager == null) { + if (securityProvider != null) { + userManager = securityProvider.getUserConfiguration().getUserManager(root, getNamePathMapper(), session); + } else { + throw new UnsupportedRepositoryOperationException("User management not supported."); + } + } + return userManager; + } + + @Nonnull + PrivilegeManager getPrivilegeManager() throws UnsupportedRepositoryOperationException { + if (privilegeManager == null) { + if (securityProvider != null) { + privilegeManager = securityProvider.getPrivilegeConfiguration().getPrivilegeManager(contentSession, root, getNamePathMapper()); + } else { + throw new UnsupportedRepositoryOperationException("Privilege management not supported."); + } + } + return privilegeManager; + } + + @Nonnull + EffectiveNodeTypeProvider getEffectiveNodeTypeProvider() throws RepositoryException { + return (EffectiveNodeTypeProvider) workspace.getNodeTypeManager(); + } + + @Nonnull + DefinitionProvider getDefinitionProvider() throws RepositoryException { + return (DefinitionProvider) workspace.getNodeTypeManager(); + } +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/SessionImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/SessionImpl.java new file mode 100644 index 00000000000..6847765892b --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/SessionImpl.java @@ -0,0 +1,575 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr; + +import java.security.AccessControlException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nonnull; +import javax.jcr.AccessDeniedException; +import javax.jcr.Credentials; +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.NamespaceException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ValueFactory; +import javax.jcr.Workspace; +import javax.jcr.retention.RetentionManager; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.commons.AbstractSession; +import org.apache.jackrabbit.commons.iterator.AccessControlPolicyIteratorAdapter; +import org.apache.jackrabbit.oak.api.TreeLocation; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.jcr.xml.XmlImportHandler; +import org.apache.jackrabbit.oak.spi.security.authentication.ImpersonationCredentials; +import org.apache.jackrabbit.oak.util.TODO; +import org.apache.jackrabbit.util.XMLChar; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.ContentHandler; + +/** + * {@code SessionImpl}... + */ +public class SessionImpl extends AbstractSession implements JackrabbitSession { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(SessionImpl.class); + + private final SessionDelegate dlg; + + /** + * Local namespace remappings. Prefixes as keys and namespace URIs as values. + *

    + * This map is only accessed from synchronized methods (see + * JCR-1793). + */ + private final Map namespaces = new HashMap(); + + SessionImpl(SessionDelegate dlg) { + this.dlg = dlg; + } + + //------------------------------------------------------------< Session >--- + + @Override + @Nonnull + public Repository getRepository() { + return dlg.getRepository(); + } + + @Override + public String getUserID() { + return dlg.getAuthInfo().getUserID(); + } + + @Override + public String[] getAttributeNames() { + return dlg.getAuthInfo().getAttributeNames(); + } + + @Override + public Object getAttribute(String name) { + return dlg.getAuthInfo().getAttribute(name); + } + + @Override + @Nonnull + public Workspace getWorkspace() { + return dlg.getWorkspace(); + } + + @Override + @Nonnull + public Session impersonate(Credentials credentials) throws RepositoryException { + ensureIsAlive(); + + ImpersonationCredentials impCreds = new ImpersonationCredentials(credentials, dlg.getAuthInfo()); + return getRepository().login(impCreds, dlg.getWorkspaceName()); + } + + @Override + @Nonnull + public ValueFactory getValueFactory() throws RepositoryException { + ensureIsAlive(); + return dlg.getValueFactory(); + } + + @Override + @Nonnull + public Node getRootNode() throws RepositoryException { + ensureIsAlive(); + + return dlg.perform(new SessionOperation() { + @Override + public NodeImpl perform() throws AccessDeniedException { + NodeDelegate nd = dlg.getRootNode(); + if (nd == null) { + throw new AccessDeniedException("Root node is not accessible."); + } else { + return new NodeImpl(nd); + } + } + }); + } + + @Override + @Nonnull + public Node getNodeByUUID(String uuid) throws RepositoryException { + return getNodeByIdentifier(uuid); + } + + @Override + @Nonnull + public Node getNodeByIdentifier(final String id) throws RepositoryException { + ensureIsAlive(); + + return dlg.perform(new SessionOperation() { + @Override + public NodeImpl perform() throws RepositoryException { + NodeDelegate d = dlg.getNodeByIdentifier(id); + if (d == null) { + throw new ItemNotFoundException("Node with id " + id + " does not exist."); + } + return new NodeImpl(d); + } + }); + } + + @Override + public Item getItem(String absPath) throws RepositoryException { + if (nodeExists(absPath)) { + return getNode(absPath); + } else { + return getProperty(absPath); + } + } + + @Override + public boolean itemExists(String absPath) throws RepositoryException { + return nodeExists(absPath) || propertyExists(absPath); + } + + @Override + public Node getNode(final String absPath) throws RepositoryException { + ensureIsAlive(); + + return dlg.perform(new SessionOperation() { + @Override + public NodeImpl perform() throws RepositoryException { + String oakPath = dlg.getOakPathOrThrow(absPath); + NodeDelegate d = dlg.getNode(oakPath); + if (d == null) { + throw new PathNotFoundException("Node with path " + absPath + " does not exist."); + } + return new NodeImpl(d); + } + }); + } + + @Override + public boolean nodeExists(final String absPath) throws RepositoryException { + ensureIsAlive(); + + return dlg.perform(new SessionOperation() { + @Override + public Boolean perform() throws RepositoryException { + String oakPath = dlg.getOakPathOrThrow(absPath); + return dlg.getNode(oakPath) != null; + } + }); + } + + @Override + public Property getProperty(final String absPath) throws RepositoryException { + if (absPath.equals("/")) { + throw new RepositoryException("The root node is not a property"); + } else { + return dlg.perform(new SessionOperation() { + @Override + public PropertyImpl perform() throws RepositoryException { + String oakPath = dlg.getOakPathOrThrowNotFound(absPath); + TreeLocation loc = dlg.getLocation(oakPath); + if (loc.getProperty() == null) { + throw new PathNotFoundException(absPath); + } + else { + return new PropertyImpl(new PropertyDelegate(dlg, loc)); + } + } + }); + } + } + + @Override + public boolean propertyExists(final String absPath) throws RepositoryException { + if (absPath.equals("/")) { + throw new RepositoryException("The root node is not a property"); + } else { + return dlg.perform(new SessionOperation() { + @Override + public Boolean perform() throws RepositoryException { + String oakPath = dlg.getOakPathOrThrowNotFound(absPath); + TreeLocation loc = dlg.getLocation(oakPath); + return loc.getProperty() != null; + } + }); + } + } + + @Override + public void move(final String srcAbsPath, final String destAbsPath) throws RepositoryException { + ensureIsAlive(); + + // FIXME: check for protection on src-parent and dest-parent (OAK-250) + dlg.perform(new SessionOperation() { + @Override + public Void perform() throws RepositoryException { + String oakPath = dlg.getOakPathKeepIndexOrThrowNotFound(destAbsPath); + String oakName = PathUtils.getName(oakPath); + // handle index + if (oakName.contains("[")) { + throw new RepositoryException("Cannot create a new node using a name including an index"); + } + + dlg.move( + dlg.getOakPathOrThrowNotFound(srcAbsPath), + dlg.getOakPathOrThrowNotFound(oakPath), + true); + + return null; + } + }); + } + + @Override + public void save() throws RepositoryException { + ensureIsAlive(); + dlg.save(); + } + + @Override + public void refresh(boolean keepChanges) throws RepositoryException { + ensureIsAlive(); + dlg.refresh(keepChanges); + } + + @Override + public boolean hasPendingChanges() throws RepositoryException { + ensureIsAlive(); + return dlg.hasPendingChanges(); + } + + @Override + public boolean isLive() { + return dlg.isAlive(); + } + + + @Override + public void logout() { + dlg.logout(); + synchronized (namespaces) { + namespaces.clear(); + } + } + + @Override + @Nonnull + public ContentHandler getImportContentHandler( + String parentAbsPath, int uuidBehavior) throws RepositoryException { + final Node parent = getNode(parentAbsPath); + return new XmlImportHandler(parent, uuidBehavior); + } + + /** + * @see javax.jcr.Session#addLockToken(String) + */ + @Override + public void addLockToken(String lt) { + try { + dlg.getLockManager().addLockToken(lt); + } catch (RepositoryException e) { + log.warn("Unable to add lock token '{}' to this session: {}", lt, e.getMessage()); + } + } + + /** + * @see javax.jcr.Session#getLockTokens() + */ + @Override + @Nonnull + public String[] getLockTokens() { + try { + return dlg.getLockManager().getLockTokens(); + } catch (RepositoryException e) { + log.warn("Unable to retrieve lock tokens for this session: {}", e.getMessage()); + return new String[0]; } + } + + /** + * @see javax.jcr.Session#removeLockToken(String) + */ + @Override + public void removeLockToken(String lt) { + try { + dlg.getLockManager().addLockToken(lt); + } catch (RepositoryException e) { + log.warn("Unable to add lock token '{}' to this session: {}", lt, e.getMessage()); + } + } + + @Override + public boolean hasPermission(String absPath, String actions) throws RepositoryException { + ensureIsAlive(); + + String oakPath = dlg.getOakPathOrNull(absPath); + if (oakPath == null) { + // TODO should we throw an exception here? + return TODO.dummyImplementation().returnValue(false); + } + + // TODO implement hasPermission + return TODO.dummyImplementation().returnValue(true); + } + + /** + * @see javax.jcr.Session#checkPermission(String, String) + */ + @Override + public void checkPermission(String absPath, String actions) throws AccessControlException, RepositoryException { + if (!hasPermission(absPath, actions)) { + throw new AccessControlException("Access control violation: path = " + absPath + ", actions = " + actions); + } + } + + @Override + public boolean hasCapability(String methodName, Object target, Object[] arguments) throws RepositoryException { + ensureIsAlive(); + + // TODO + return false; + } + + @Override + @Nonnull + public AccessControlManager getAccessControlManager() + throws RepositoryException { + return TODO.dummyImplementation().returnValue(new AccessControlManager() { + @Override + public void setPolicy(String absPath, AccessControlPolicy policy) { + throw new AccessControlException(policy.toString()); + } + @Override + public void removePolicy(String absPath, AccessControlPolicy policy) { + throw new AccessControlException(policy.toString()); + } + @Override + public Privilege privilegeFromName(String privilegeName) + throws AccessControlException { + throw new AccessControlException(privilegeName); + } + @Override + public boolean hasPrivileges(String absPath, Privilege[] privileges) { + return true; + } + @Override + public Privilege[] getSupportedPrivileges(String absPath) { + return new Privilege[0]; + } + @Override + public Privilege[] getPrivileges(String absPath) { + return new Privilege[0]; + } + @Override + public AccessControlPolicy[] getPolicies(String absPath) { + return new AccessControlPolicy[0]; + } + @Override + public AccessControlPolicy[] getEffectivePolicies(String absPath) { + return new AccessControlPolicy[0]; + } + @Override + public AccessControlPolicyIterator getApplicablePolicies(String absPath) { + return AccessControlPolicyIteratorAdapter.EMPTY; + } + }); + } + + /** + * @see javax.jcr.Session#getRetentionManager() + */ + @Override + @Nonnull + public RetentionManager getRetentionManager() throws RepositoryException { + throw new UnsupportedRepositoryOperationException("Retention Management is not supported."); + } + + //---------------------------------------------------------< Namespaces >--- + // The code below was initially copied from JCR Commons AbstractSession, but + // provides information the "hasRemappings" information + + @Override + public void setNamespacePrefix(String prefix, String uri) throws RepositoryException { + if (prefix == null) { + throw new IllegalArgumentException("Prefix must not be null"); + } else if (uri == null) { + throw new IllegalArgumentException("Namespace must not be null"); + } else if (prefix.isEmpty()) { + throw new NamespaceException( + "Empty prefix is reserved and can not be remapped"); + } else if (uri.isEmpty()) { + throw new NamespaceException( + "Default namespace is reserved and can not be remapped"); + } else if (prefix.toLowerCase(Locale.ENGLISH).startsWith("xml")) { + throw new NamespaceException( + "XML prefixes are reserved: " + prefix); + } else if (!XMLChar.isValidNCName(prefix)) { + throw new NamespaceException( + "Prefix is not a valid XML NCName: " + prefix); + } + + synchronized (namespaces) { + // Remove existing mapping for the given prefix + namespaces.remove(prefix); + + // Remove existing mapping(s) for the given URI + Set prefixes = new HashSet(); + for (Map.Entry entry : namespaces.entrySet()) { + if (entry.getValue().equals(uri)) { + prefixes.add(entry.getKey()); + } + } + namespaces.keySet().removeAll(prefixes); + + // Add the new mapping + namespaces.put(prefix, uri); + } + } + + @Override + public String[] getNamespacePrefixes() throws RepositoryException { + Set uris = new HashSet(); + uris.addAll(Arrays.asList(getWorkspace().getNamespaceRegistry().getURIs())); + synchronized (namespaces) { + // Add namespace uris only visible to session + uris.addAll(namespaces.values()); + } + Set prefixes = new HashSet(); + for (String uri : uris) { + prefixes.add(getNamespacePrefix(uri)); + } + return prefixes.toArray(new String[prefixes.size()]); + } + + @Override + public String getNamespaceURI(String prefix) throws RepositoryException { + synchronized (namespaces) { + String uri = namespaces.get(prefix); + + if (uri == null) { + // Not in local mappings, try the global ones + uri = getWorkspace().getNamespaceRegistry().getURI(prefix); + if (namespaces.containsValue(uri)) { + // The global URI is locally mapped to some other prefix, + // so there are no mappings for this prefix + throw new NamespaceException("Namespace not found: " + prefix); + } + } + + return uri; + } + } + + @Override + public String getNamespacePrefix(String uri) throws RepositoryException { + synchronized (namespaces) { + for (Map.Entry entry : namespaces.entrySet()) { + if (entry.getValue().equals(uri)) { + return entry.getKey(); + } + } + + // The following throws an exception if the URI is not found, that's OK + String prefix = getWorkspace().getNamespaceRegistry().getPrefix(uri); + + // Generate a new prefix if the global mapping is already taken + String base = prefix; + for (int i = 2; namespaces.containsKey(prefix); i++) { + prefix = base + i; + } + + if (!base.equals(prefix)) { + namespaces.put(prefix, uri); + } + return prefix; + } + } + + boolean hasSessionLocalMappings() { + return !namespaces.isEmpty(); + } + + //--------------------------------------------------< JackrabbitSession >--- + + @Override + @Nonnull + public PrincipalManager getPrincipalManager() throws RepositoryException { + return dlg.getPrincipalManager(); + } + + @Override + @Nonnull + public UserManager getUserManager() throws RepositoryException { + return dlg.getUserManager(); + } + + //------------------------------------------------------------< private >--- + + /** + * Ensure that this session is alive and throw an exception otherwise. + * + * @throws RepositoryException if this session has been rendered invalid + * for some reason (e.g. if this session has been closed explicitly by logout) + */ + private void ensureIsAlive() throws RepositoryException { + // check session status + if (!dlg.isAlive()) { + throw new RepositoryException("This session has been closed."); + } + } +} \ No newline at end of file diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/SessionNameMapper.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/SessionNameMapper.java new file mode 100644 index 00000000000..2185af2891f --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/SessionNameMapper.java @@ -0,0 +1,60 @@ +package org.apache.jackrabbit.oak.jcr; + +import javax.annotation.CheckForNull; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.oak.namepath.AbstractNameMapper; + +public class SessionNameMapper extends AbstractNameMapper { + private final SessionDelegate sessionDelegate; + + public SessionNameMapper(SessionDelegate sessionDelegate) { + this.sessionDelegate = sessionDelegate; + } + + @Override + @CheckForNull + protected String getJcrPrefix(String oakPrefix) { + try { + String ns = sessionDelegate.getWorkspace().getNamespaceRegistry().getURI(oakPrefix); + return sessionDelegate.getSession().getNamespacePrefix(ns); + } catch (RepositoryException e) { + SessionDelegate.log.debug("Could not get JCR prefix for OAK prefix " + oakPrefix); + return null; + } + } + + @Override + @CheckForNull + protected String getOakPrefix(String jcrPrefix) { + try { + String ns = sessionDelegate.getSession().getNamespaceURI(jcrPrefix); + return sessionDelegate.getWorkspace().getNamespaceRegistry().getPrefix(ns); + } catch (RepositoryException e) { + SessionDelegate.log.debug("Could not get OAK prefix for JCR prefix " + jcrPrefix); + return null; + } + } + + @Override + @CheckForNull + protected String getOakPrefixFromURI(String uri) { + try { + return sessionDelegate.getWorkspace().getNamespaceRegistry().getPrefix(uri); + } catch (RepositoryException e) { + SessionDelegate.log.debug("Could not get OAK prefix for URI " + uri); + return null; + } + } + + @Override + public boolean hasSessionLocalMappings() { + if (sessionDelegate.getSession() instanceof SessionImpl) { + return ((SessionImpl) sessionDelegate.getSession()).hasSessionLocalMappings(); + } + else { + // we don't know + return true; + } + } +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/SessionOperation.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/SessionOperation.java new file mode 100644 index 00000000000..14fad8449f6 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/SessionOperation.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.jcr; + +import javax.jcr.RepositoryException; + +/** + * A {@code SessionOperation} provides an execution context for executing session scoped operations. + */ +public interface SessionOperation { + T perform() throws RepositoryException; +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/WorkspaceImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/WorkspaceImpl.java new file mode 100644 index 00000000000..15098e03560 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/WorkspaceImpl.java @@ -0,0 +1,295 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr; + +import java.io.IOException; +import java.io.InputStream; + +import javax.annotation.Nonnull; +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ValueFactory; +import javax.jcr.lock.LockManager; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.observation.ObservationManager; +import javax.jcr.query.QueryManager; +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; + +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.jcr.lock.LockManagerImpl; +import org.apache.jackrabbit.oak.jcr.query.QueryManagerImpl; +import org.apache.jackrabbit.oak.jcr.version.VersionManagerImpl; +import org.apache.jackrabbit.oak.namepath.NameMapper; +import org.apache.jackrabbit.oak.plugins.name.ReadWriteNamespaceRegistry; +import org.apache.jackrabbit.oak.plugins.nodetype.ReadWriteNodeTypeManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; + +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.NODE_TYPES_PATH; + +/** + * {@code WorkspaceImpl}... + */ +public class WorkspaceImpl implements JackrabbitWorkspace { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(WorkspaceImpl.class); + + private final SessionDelegate sessionDelegate; + private final QueryManagerImpl queryManager; + + private final LockManager lockManager; + + public WorkspaceImpl(SessionDelegate sessionDelegate) + throws RepositoryException { + this.sessionDelegate = sessionDelegate; + this.queryManager = new QueryManagerImpl(sessionDelegate); + this.lockManager = new LockManagerImpl(sessionDelegate.getSession()); + } + + //----------------------------------------------------------< Workspace >--- + @Override + public Session getSession() { + return sessionDelegate.getSession(); + } + + @Override + public String getName() { + return sessionDelegate.getWorkspaceName(); + } + + @Override + public void copy(String srcAbsPath, String destAbsPath) throws RepositoryException { + copy(getName(), srcAbsPath, destAbsPath); + } + + @Override + public void copy(String srcWorkspace, String srcAbsPath, String destAbsPath) throws RepositoryException { + ensureIsAlive(); + + if (!getName().equals(srcWorkspace)) { + throw new UnsupportedRepositoryOperationException("Not implemented."); + } + + // FIXME: check for protection on src-parent and dest-parent (OAK-250) + + String oakPath = sessionDelegate.getOakPathKeepIndexOrThrowNotFound(destAbsPath); + String oakName = PathUtils.getName(oakPath); + // handle index + if (oakName.contains("[")) { + throw new RepositoryException("Cannot create a new node using a name including an index"); + } + + sessionDelegate.copy( + sessionDelegate.getOakPathOrThrowNotFound(srcAbsPath), + sessionDelegate.getOakPathOrThrowNotFound(oakPath)); + } + + @Override + public void clone(String srcWorkspace, String srcAbsPath, String destAbsPath, boolean removeExisting) throws RepositoryException { + ensureIsAlive(); + + // TODO + // FIXME: check for protection on src-parent and dest-parent (OAK-250) + + throw new UnsupportedRepositoryOperationException("Not implemented."); + } + + @Override + public void move(String srcAbsPath, String destAbsPath) throws RepositoryException { + ensureIsAlive(); + + // FIXME: check for protection on src-parent and dest-parent (OAK-250) + + String oakPath = sessionDelegate.getOakPathKeepIndexOrThrowNotFound(destAbsPath); + String oakName = PathUtils.getName(oakPath); + // handle index + if (oakName.contains("[")) { + throw new RepositoryException("Cannot create a new node using a name including an index"); + } + + sessionDelegate.move( + sessionDelegate.getOakPathOrThrowNotFound(srcAbsPath), + sessionDelegate.getOakPathOrThrowNotFound(oakPath), + false); + } + + @Override + public void restore(Version[] versions, boolean removeExisting) throws RepositoryException { + getVersionManager().restore(versions, removeExisting); + } + + @Override + public LockManager getLockManager() { + return lockManager; + } + + @Override + public QueryManager getQueryManager() throws RepositoryException { + ensureIsAlive(); + return queryManager; + } + + @Override + public NamespaceRegistry getNamespaceRegistry() { + return new ReadWriteNamespaceRegistry() { + @Override + protected Tree getReadTree() { + return sessionDelegate.getRoot().getTree("/"); + } + @Override + protected Root getWriteRoot() { + return sessionDelegate.getContentSession().getLatestRoot(); + } + @Override + protected void refresh() throws RepositoryException { + getSession().refresh(true); + } + }; + } + + @Override + public NodeTypeManager getNodeTypeManager() { + return new ReadWriteNodeTypeManager() { + @Override + protected void refresh() throws RepositoryException { + getSession().refresh(true); + } + + @Override + protected Tree getTypes() { + return sessionDelegate.getRoot().getTree(NODE_TYPES_PATH); + } + + @Nonnull + @Override + protected Root getWriteRoot() { + return sessionDelegate.getContentSession().getLatestRoot(); + } + + @Override + protected ValueFactory getValueFactory() { + return sessionDelegate.getValueFactory(); + } + + @Nonnull + @Override + protected NameMapper getNameMapper() { + return sessionDelegate.getNamePathMapper(); + } + }; + } + + @Override + public ObservationManager getObservationManager() throws RepositoryException { + ensureIsAlive(); + + return sessionDelegate.getObservationManager(); + } + + @Override + public VersionManager getVersionManager() { + return new VersionManagerImpl(sessionDelegate); + } + + @Override + public String[] getAccessibleWorkspaceNames() throws RepositoryException { + ensureIsAlive(); + + // TODO -> SPI + return new String[] {getName()}; + } + + @Override + public ContentHandler getImportContentHandler(String parentAbsPath, int uuidBehavior) throws RepositoryException { + ensureIsAlive(); + + // TODO + throw new UnsupportedRepositoryOperationException("TODO: Workspace.getImportContentHandler"); + } + + @Override + public void importXML(String parentAbsPath, InputStream in, int uuidBehavior) throws IOException, RepositoryException { + ensureIsAlive(); + + // TODO -> SPI + throw new UnsupportedRepositoryOperationException("TODO: Workspace.importXML"); + } + + @Override + public void createWorkspace(String name) throws RepositoryException { + ensureIsAlive(); + + // TODO -> SPI + throw new UnsupportedRepositoryOperationException("TODO: Workspace.createWorkspace"); + } + + @Override + public void createWorkspace(String name, String srcWorkspace) throws RepositoryException { + ensureIsAlive(); + + // TODO -> SPI + throw new UnsupportedRepositoryOperationException("TODO: Workspace.createWorkspace"); + } + + @Override + public void deleteWorkspace(String name) throws RepositoryException { + ensureIsAlive(); + + // TODO -> SPI + throw new UnsupportedRepositoryOperationException("TODO: Workspace.deleteWorkspace"); + } + + //------------------------------------------------< JackrabbitWorkspace >--- + + @Override + public void createWorkspace(String workspaceName, InputSource workspaceTemplate) throws RepositoryException { + ensureIsAlive(); + + // TODO -> SPI + throw new UnsupportedRepositoryOperationException("TODO: Workspace.createWorkspace"); + } + + /** + * @see org.apache.jackrabbit.api.JackrabbitWorkspace#getPrivilegeManager() + */ + @Override + public PrivilegeManager getPrivilegeManager() throws RepositoryException { + return sessionDelegate.getPrivilegeManager(); + } + + //------------------------------------------------------------< private >--- + + private void ensureIsAlive() throws RepositoryException { + // check session status + if (!sessionDelegate.isAlive()) { + throw new RepositoryException("This session has been closed."); + } + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/lock/LockManagerImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/lock/LockManagerImpl.java new file mode 100644 index 00000000000..5ac5c1577c2 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/lock/LockManagerImpl.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.lock; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; +import javax.jcr.lock.LockManager; + +/** + * Simple lock manager implementation that just keeps track of a set of lock + * tokens and delegates all locking operations back to the {@link Session} + * and {@link Node} implementations. + */ +public class LockManagerImpl implements LockManager { + + private final Session session; + + private final Set tokens = new HashSet(); + + public LockManagerImpl(Session session) { + this.session = session; + } + + @Override + public String[] getLockTokens() throws RepositoryException { + String[] array = tokens.toArray(new String[tokens.size()]); + Arrays.sort(array); + return array; + } + + @Override + public void addLockToken(String lockToken) throws RepositoryException { + tokens.add(lockToken); + } + + @Override + public void removeLockToken(String lockToken) throws RepositoryException { + if (!tokens.remove(lockToken)) { + throw new LockException( + "Lock token " + lockToken + " is not held by this session"); + } + } + + @Override + public boolean isLocked(String absPath) throws RepositoryException { + return session.getNode(absPath).isLocked(); + } + + @Override + @SuppressWarnings("deprecation") + public boolean holdsLock(String absPath) throws RepositoryException { + return session.getNode(absPath).holdsLock(); + } + + @Override + @SuppressWarnings("deprecation") + public Lock getLock(String absPath) throws RepositoryException { + return session.getNode(absPath).getLock(); + } + + @Override + @SuppressWarnings("deprecation") + public Lock lock( + String absPath, boolean isDeep, boolean isSessionScoped, + long timeoutHint, String ownerInfo) throws RepositoryException { + return session.getNode(absPath).lock(isDeep, isSessionScoped); + } + + @Override + @SuppressWarnings("deprecation") + public void unlock(String absPath) throws RepositoryException { + session.getNode(absPath).unlock(); + } + +} \ No newline at end of file diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/osgi/Activator.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/osgi/Activator.java new file mode 100644 index 00000000000..4ef642ad796 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/osgi/Activator.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.osgi; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import javax.jcr.Repository; + +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +public class Activator implements BundleActivator, ServiceTrackerCustomizer { + + private BundleContext context; + + private ScheduledExecutorService executor; + + private SecurityProvider securityProvider; // TODO + + private ServiceTracker tracker; + + private final Map services = + new HashMap(); + + //-----------------------------------------------------< BundleActivator >-- + + @Override + public void start(BundleContext bundleContext) throws Exception { + context = bundleContext; + executor = Executors.newScheduledThreadPool(1); + tracker = new ServiceTracker( + context, ContentRepository.class.getName(), this); + tracker.open(); + } + + @Override + public void stop(BundleContext bundleContext) throws Exception { + tracker.close(); + executor.shutdown(); + } + + //--------------------------------------------< ServiceTrackerCustomizer >-- + + @Override + public Object addingService(ServiceReference reference) { + Object service = context.getService(reference); + if (service instanceof ContentRepository) { + ContentRepository repository = (ContentRepository) service; + services.put(reference, context.registerService( + Repository.class.getName(), + new OsgiRepository(repository, executor, securityProvider), + new Properties())); + return service; + } else { + context.ungetService(reference); + return null; + } + } + + @Override + public void modifiedService(ServiceReference reference, Object service) { + } + + @Override + public void removedService(ServiceReference reference, Object service) { + services.get(reference).unregister(); + context.ungetService(reference); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/osgi/OsgiRepository.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/osgi/OsgiRepository.java new file mode 100644 index 00000000000..00a601ce03b --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/osgi/OsgiRepository.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.jcr.osgi; + +import java.util.concurrent.ScheduledExecutorService; + +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.jcr.RepositoryImpl; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; + +/** + * Workaround to a JAAS class loading issue in OSGi environments. + * + * @see OAK-256 + */ +public class OsgiRepository extends RepositoryImpl { + + public OsgiRepository(ContentRepository repository, + ScheduledExecutorService executor, + SecurityProvider securityProvider) { + super(repository, executor, securityProvider); + } + + @Override + public Session login(Credentials credentials, String workspace) + throws RepositoryException { + // TODO: The context class loader hack below shouldn't be needed + // with a properly OSGi-compatible JAAS implementation + Thread thread = Thread.currentThread(); + ClassLoader loader = thread.getContextClassLoader(); + try { + thread.setContextClassLoader(Oak.class.getClassLoader()); + return super.login(credentials, workspace); + } finally { + thread.setContextClassLoader(loader); + } + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/PrefetchIterator.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/PrefetchIterator.java new file mode 100644 index 00000000000..7e53325a4a7 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/PrefetchIterator.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * An iterator that pre-fetches a number of items in order to calculate the size + * of the result if possible. This iterator loads at least a number of items, + * and then tries to load some more items until the timeout is reached or the + * maximum number of entries are read. + *

    + * Prefeching is only done when size() is called. + * + * @param the iterator data type + */ +public class PrefetchIterator implements Iterator { + + private final Iterator it; + private final long minPrefetch, timeout, maxPrefetch; + private boolean prefetchDone; + private Iterator prefetchIterator; + private long size, position; + + /** + * Create a new iterator. + * + * @param it the base iterator + * @param min the minimum number of items to pre-fetch + * @param timeout the maximum time to pre-fetch in milliseconds + * @param max the maximum number of items to pre-fetch + * @param size the size (prefetching is only required if -1) + */ + PrefetchIterator(Iterator it, long min, long timeout, long max, long size) { + this.it = it; + this.minPrefetch = min; + this.timeout = timeout; + this.maxPrefetch = max; + this.size = size; + } + + @Override + public boolean hasNext() { + if (prefetchIterator != null) { + if (prefetchIterator.hasNext()) { + return true; + } + prefetchIterator = null; + } + boolean result = it.hasNext(); + if (!result) { + size = position; + } + return result; + } + + @Override + public K next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + if (prefetchIterator != null) { + return prefetchIterator.next(); + } + position++; + return it.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + /** + * Get the size if known. This call might pre-fetch data. The returned value + * is unknown if the actual size is larger than the number of pre-fetched + * elements, unless the end of the iterator has been reached already. + * + * @return the size, or -1 if unknown + */ + public long size() { + if (size != -1 || prefetchDone || position > maxPrefetch) { + return size; + } + prefetchDone = true; + ArrayList list = new ArrayList(); + long end; + if (timeout <= 0) { + end = 0; + } else { + long nanos = System.nanoTime(); + end = nanos + timeout * 1000 * 1000; + } + while (position <= maxPrefetch) { + if (position > minPrefetch) { + if (end == 0 || System.nanoTime() > end) { + break; + } + } + if (!it.hasNext()) { + size = position; + break; + } + position++; + list.add(it.next()); + } + if (list.size() > 0) { + prefetchIterator = list.iterator(); + position -= list.size(); + } + return size; + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/QueryImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/QueryImpl.java new file mode 100644 index 00000000000..f0586587290 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/QueryImpl.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query; + +import java.util.HashMap; +import java.util.List; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeType; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.jcr.NodeDelegate; +import org.apache.jackrabbit.oak.jcr.NodeImpl; +import org.apache.jackrabbit.oak.jcr.SessionDelegate; +import org.apache.jackrabbit.oak.plugins.value.ValueFactoryImpl; + +/** + * The implementation of the corresponding JCR interface. + */ +public class QueryImpl implements Query { + + private final QueryManagerImpl manager; + private final HashMap bindVariableMap = new HashMap(); + private final String language; + private final String statement; + private long limit = Long.MAX_VALUE; + private long offset; + private boolean parsed; + private String storedQueryPath; + + QueryImpl(QueryManagerImpl manager, String statement, String language) { + this.manager = manager; + this.statement = statement; + this.language = language; + } + + void setStoredQueryPath(String storedQueryPath) { + this.storedQueryPath = storedQueryPath; + } + + @Override + public void bindValue(String varName, Value value) throws RepositoryException { + parse(); + if (!bindVariableMap.containsKey(varName)) { + throw new IllegalArgumentException("Variable name " + varName + " is not a valid variable in this query"); + } + bindVariableMap.put(varName, value); + } + + private void parse() throws InvalidQueryException { + if (parsed) { + return; + } + List names = manager.parse(statement, language); + for (String n : names) { + bindVariableMap.put(n, null); + } + parsed = true; + } + + @Override + public QueryResult execute() throws RepositoryException { + return manager.executeQuery(statement, language, limit, offset, bindVariableMap); + } + + @Override + public String[] getBindVariableNames() throws RepositoryException { + parse(); + String[] names = new String[bindVariableMap.size()]; + bindVariableMap.keySet().toArray(names); + return names; + } + + @Override + public String getLanguage() { + return language; + } + + @Override + public String getStatement() { + return statement; + } + + @Override + public String getStoredQueryPath() throws RepositoryException { + if (storedQueryPath == null) { + throw new ItemNotFoundException("Not a stored query"); + } + return storedQueryPath; + } + + @Override + public void setLimit(long limit) { + this.limit = limit; + } + + @Override + public void setOffset(long offset) { + this.offset = offset; + } + + @Override + public Node storeAsNode(String absPath) throws RepositoryException { + manager.ensureIsAlive(); + SessionDelegate sessionDelegate = manager.getSessionDelegate(); + String oakPath = sessionDelegate.getOakPathOrThrow(absPath); + // TODO query nodes should be of type nt:query + String parent = PathUtils.getParentPath(oakPath); + String nodeName = PathUtils.getName(oakPath); + NodeDelegate parentNode = sessionDelegate.getNode(parent); + ValueFactoryImpl vf = sessionDelegate.getValueFactory(); + if (parentNode == null) { + throw new PathNotFoundException("The specified path does not exist: " + parent); + } + NodeDelegate node = parentNode.addChild(nodeName); + if (node == null) { + throw new ItemExistsException("Node already exists: " + absPath); + } + node.setProperty("statement", vf.createValue(statement)); + node.setProperty("language", vf.createValue(language)); + NodeImpl n = new NodeImpl(node); + n.setPrimaryType(NodeType.NT_QUERY); + storedQueryPath = oakPath; + return n; + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/QueryManagerImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/QueryManagerImpl.java new file mode 100644 index 00000000000..8012d911380 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/QueryManagerImpl.java @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeType; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; +import javax.jcr.query.QueryResult; +import javax.jcr.query.qom.QueryObjectModelFactory; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Result; +import org.apache.jackrabbit.oak.api.SessionQueryEngine; +import org.apache.jackrabbit.oak.jcr.SessionDelegate; +import org.apache.jackrabbit.oak.jcr.query.qom.QueryObjectModelFactoryImpl; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.plugins.memory.PropertyStates; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; + +/** + * The implementation of the corresponding JCR interface. + */ +public class QueryManagerImpl implements QueryManager { + + private final QueryObjectModelFactoryImpl qomFactory; + private final SessionQueryEngine queryEngine; + private final SessionDelegate sessionDelegate; + private final HashSet supportedQueryLanguages = new HashSet(); + + public QueryManagerImpl(SessionDelegate sessionDelegate) { + this.sessionDelegate = sessionDelegate; + qomFactory = new QueryObjectModelFactoryImpl(this, sessionDelegate.getValueFactory()); + queryEngine = sessionDelegate.getQueryEngine(); + supportedQueryLanguages.addAll(queryEngine.getSupportedQueryLanguages()); + } + + @Override + public QueryImpl createQuery(String statement, String language) throws RepositoryException { + if (!supportedQueryLanguages.contains(language)) { + throw new InvalidQueryException("The specified language is not supported: " + language); + } + return new QueryImpl(this, statement, language); + } + + @Override + public QueryObjectModelFactory getQOMFactory() { + return qomFactory; + } + + @Override + public Query getQuery(Node node) throws RepositoryException { + if (!node.isNodeType(NodeType.NT_QUERY)) { + throw new InvalidQueryException("Not an nt:query node: " + node.getPath()); + } + String statement = node.getProperty("statement").getString(); + String language = node.getProperty("language").getString(); + QueryImpl query = createQuery(statement, language); + query.setStoredQueryPath(node.getPath()); + return query; + } + + @Override + public String[] getSupportedQueryLanguages() throws RepositoryException { + ArrayList list = new ArrayList(queryEngine.getSupportedQueryLanguages()); + // JQOM is supported in this level only (converted to JCR_SQL2) + list.add(Query.JCR_JQOM); + // create a new instance each time because the array is mutable + // (the caller could modify it) + return list.toArray(new String[list.size()]); + } + + /** + * Parse the query and get the bind variable names. + * + * @param statement the query statement + * @param language the query language + * @return the bind variable names + */ + public List parse(String statement, String language) throws InvalidQueryException { + try { + return queryEngine.getBindVariableNames(statement, language); + } catch (ParseException e) { + throw new InvalidQueryException(e); + } + } + + public QueryResult executeQuery(String statement, String language, + long limit, long offset, HashMap bindVariableMap) throws RepositoryException { + try { + Map bindMap = convertMap(bindVariableMap); + NamePathMapper namePathMapper = sessionDelegate.getNamePathMapper(); + Result r = queryEngine.executeQuery(statement, language, limit, offset, + bindMap, namePathMapper); + return new QueryResultImpl(sessionDelegate, r); + } catch (IllegalArgumentException e) { + throw new InvalidQueryException(e); + } catch (ParseException e) { + throw new InvalidQueryException(e); + } + } + + private Map convertMap( + HashMap bindVariableMap) throws RepositoryException { + HashMap map = new HashMap(); + for (Entry e : bindVariableMap.entrySet()) { + map.put(e.getKey(), + PropertyValues.create(PropertyStates.createProperty("", + e.getValue()))); + } + return map; + } + + public SessionDelegate getSessionDelegate() { + return sessionDelegate; + } + + void ensureIsAlive() throws RepositoryException { + // check session status + if (!sessionDelegate.isAlive()) { + throw new RepositoryException("This session has been closed."); + } + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/QueryResultImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/QueryResultImpl.java new file mode 100644 index 00000000000..3d265e342c5 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/QueryResultImpl.java @@ -0,0 +1,237 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import javax.annotation.CheckForNull; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.query.QueryResult; +import javax.jcr.query.RowIterator; + +import org.apache.jackrabbit.commons.iterator.NodeIteratorAdapter; +import org.apache.jackrabbit.commons.iterator.RowIteratorAdapter; +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Result; +import org.apache.jackrabbit.oak.api.ResultRow; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.jcr.NodeDelegate; +import org.apache.jackrabbit.oak.jcr.NodeImpl; +import org.apache.jackrabbit.oak.jcr.SessionDelegate; +import org.apache.jackrabbit.oak.plugins.value.ValueFactoryImpl; + +/** + * The implementation of the corresponding JCR interface. + */ +public class QueryResultImpl implements QueryResult { + + /** + * The minimum number of rows / nodes to pre-fetch. + */ + private static final int PREFETCH_MIN = 20; + + /** + * The maximum number of rows / nodes to pre-fetch. + */ + private static final int PREFETCH_MAX = 100; + + /** + * The maximum number of milliseconds to prefetch rows / nodes. + */ + private static final int PREFETCH_TIMEOUT = 100; + + final SessionDelegate sessionDelegate; + final Result result; + final String pathFilter; + + public QueryResultImpl(SessionDelegate sessionDelegate, Result result) { + this.sessionDelegate = sessionDelegate; + this.result = result; + + // TODO the path currently contains the workspace name + // TODO filter in oak-core once we support workspaces there + pathFilter = "/"; + } + + @Override + public String[] getColumnNames() throws RepositoryException { + return result.getColumnNames(); + } + + @Override + public String[] getSelectorNames() { + return result.getSelectorNames(); + } + + boolean includeRow(String path) { + if (path == null) { + // a row without path (explain,...) + return true; + } + if (PathUtils.isAncestor(pathFilter, path) || pathFilter.equals(path)) { + // a row within this workspace + return true; + } + return false; + } + + @Override + public RowIterator getRows() throws RepositoryException { + Iterator rowIterator = new Iterator() { + + private final Iterator it = result.getRows().iterator(); + private RowImpl current; + + { + fetch(); + } + + private void fetch() { + current = null; + while (it.hasNext()) { + ResultRow r = it.next(); + for (String s : getSelectorNames()) { + String path = r.getPath(s); + if (includeRow(path)) { + current = new RowImpl(QueryResultImpl.this, r); + return; + } + } + } + } + + @Override + public boolean hasNext() { + return current != null; + } + + @Override + public RowImpl next() { + if (current == null) { + throw new NoSuchElementException(); + } + RowImpl r = current; + fetch(); + return r; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + }; + final PrefetchIterator prefIt = new PrefetchIterator( + rowIterator, + PREFETCH_MIN, PREFETCH_TIMEOUT, PREFETCH_MAX, + result.getSize()); + return new RowIteratorAdapter(prefIt) { + @Override + public long getSize() { + return prefIt.size(); + } + }; + } + + @CheckForNull + NodeImpl getNode(String path) { + NodeDelegate d = sessionDelegate.getNode(path); + return d == null ? null : new NodeImpl(d); + } + + String getLocalPath(String path) { + return PathUtils.concat("/", PathUtils.relativize(pathFilter, path)); + } + + @Override + public NodeIterator getNodes() throws RepositoryException { + String[] selectorNames = getSelectorNames(); + final String selectorName = selectorNames[0]; + if (getSelectorNames().length > 1) { + // use the first selector + // TODO verify using the first selector is allowed according to the specification, + // otherwise just allow it when using XPath queries, or make XPath queries + // look like they only contain one selector + // throw new RepositoryException("Query contains more than one selector: " + + // Arrays.toString(getSelectorNames())); + } + Iterator nodeIterator = new Iterator() { + + private final Iterator it = result.getRows().iterator(); + private NodeImpl current; + + { + fetch(); + } + + private void fetch() { + current = null; + while (it.hasNext()) { + ResultRow r = it.next(); + String path = r.getPath(selectorName); + if (includeRow(path)) { + current = getNode(getLocalPath(path)); + break; + } + } + } + + @Override + public boolean hasNext() { + return current != null; + } + + @Override + public NodeImpl next() { + if (current == null) { + throw new NoSuchElementException(); + } + NodeImpl n = current; + fetch(); + return n; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + }; + final PrefetchIterator prefIt = new PrefetchIterator( + nodeIterator, + PREFETCH_MIN, PREFETCH_TIMEOUT, PREFETCH_MAX, + result.getSize()); + return new NodeIteratorAdapter(prefIt) { + @Override + public long getSize() { + return prefIt.size(); + } + }; + } + + Value createValue(PropertyValue value) { + return value == null + ? null + : ValueFactoryImpl.createValue(value, sessionDelegate.getNamePathMapper()); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/RowImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/RowImpl.java new file mode 100644 index 00000000000..762c1d001d0 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/RowImpl.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.query.Row; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.ResultRow; + +/** + * The implementation of the corresponding JCR interface. + */ +public class RowImpl implements Row { + + private final QueryResultImpl result; + private final ResultRow row; + + public RowImpl(QueryResultImpl result, ResultRow row) { + this.result = result; + this.row = row; + } + + @Override + public Node getNode() throws RepositoryException { + return result.getNode(getPath()); + } + + @Override + public Node getNode(String selectorName) throws RepositoryException { + return result.getNode(getPath(selectorName)); + } + + @Override + public String getPath() throws RepositoryException { + try { + return result.getLocalPath(row.getPath()); + } catch (IllegalArgumentException e) { + throw new RepositoryException(e); + } + } + + @Override + public String getPath(String selectorName) throws RepositoryException { + try { + return result.getLocalPath(row.getPath(selectorName)); + } catch (IllegalArgumentException e) { + throw new RepositoryException(e); + } + } + + @Override + public double getScore() throws RepositoryException { + // TODO row score + return 0; + } + + @Override + public double getScore(String selectorName) throws RepositoryException { + // TODO row score + return 0; + } + + @Override + public Value getValue(String columnName) throws RepositoryException { + try { + return result.createValue(row.getValue(columnName)); + } catch (IllegalArgumentException e) { + throw new RepositoryException(e); + } + } + + @Override + public Value[] getValues() throws RepositoryException { + PropertyValue[] values = row.getValues(); + int len = values.length; + Value[] v2 = new Value[values.length]; + for (int i = 0; i < len; i++) { + v2[i] = result.createValue(values[i]); + } + return v2; + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/AndImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/AndImpl.java new file mode 100644 index 00000000000..49cf0e167e6 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/AndImpl.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.jcr.query.qom; + +import javax.jcr.query.qom.And; + +/** + * The implementation of the corresponding JCR interface. + */ +public class AndImpl extends ConstraintImpl implements And { + + private final ConstraintImpl constraint1, constraint2; + + public AndImpl(ConstraintImpl constraint1, ConstraintImpl constraint2) { + this.constraint1 = constraint1; + this.constraint2 = constraint2; + } + + @Override + public ConstraintImpl getConstraint1() { + return constraint1; + } + + @Override + public ConstraintImpl getConstraint2() { + return constraint2; + } + + @Override + public String toString() { + return protect(constraint1) + " AND " + protect(constraint2); + } + + @Override + public void bindVariables(QueryObjectModelImpl qom) { + constraint1.bindVariables(qom); + constraint2.bindVariables(qom); + } + +} + diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/BindVariableValueImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/BindVariableValueImpl.java new file mode 100644 index 00000000000..2101fca847e --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/BindVariableValueImpl.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.jcr.query.qom; + +import javax.jcr.query.qom.BindVariableValue; + +/** + * The implementation of the corresponding JCR interface. + */ +public class BindVariableValueImpl extends StaticOperandImpl implements BindVariableValue { + + private final String bindVariableName; + + public BindVariableValueImpl(String bindVariableName) { + this.bindVariableName = bindVariableName; + } + + @Override + public String getBindVariableName() { + return bindVariableName; + } + + @Override + public String toString() { + return '$' + bindVariableName; + } + + @Override + public void bindVariables(QueryObjectModelImpl qom) { + qom.addBindVariable(this); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/ChildNodeImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/ChildNodeImpl.java new file mode 100644 index 00000000000..3f14ea34325 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/ChildNodeImpl.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.query.qom.ChildNode; + +/** + * The implementation of the corresponding JCR interface. + */ +public class ChildNodeImpl extends ConstraintImpl implements ChildNode { + + private final String selectorName; + private final String parentPath; + + public ChildNodeImpl(String selectorName, String parentPath) { + this.selectorName = selectorName; + this.parentPath = parentPath; + } + + @Override + public String getSelectorName() { + return selectorName; + } + + @Override + public String getParentPath() { + return parentPath; + } + + @Override + public String toString() { + StringBuilder buff = new StringBuilder(); + buff.append("ISCHILDNODE("); + if (selectorName != null) { + buff.append(quoteSelectorName(selectorName)).append(", "); + } + buff.append(quotePath(parentPath)).append(')'); + return buff.toString(); + } + + @Override + public void bindVariables(QueryObjectModelImpl qom) { + // ignore + } + +} \ No newline at end of file diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/ChildNodeJoinConditionImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/ChildNodeJoinConditionImpl.java new file mode 100644 index 00000000000..b27d734a3e3 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/ChildNodeJoinConditionImpl.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.jcr.query.qom; + +import javax.jcr.query.qom.ChildNodeJoinCondition; + +/** + * The implementation of the corresponding JCR interface. + */ +public class ChildNodeJoinConditionImpl extends JoinConditionImpl implements ChildNodeJoinCondition { + + private final String childSelectorName; + private final String parentSelectorName; + + public ChildNodeJoinConditionImpl(String childSelectorName, String parentSelectorName) { + this.childSelectorName = childSelectorName; + this.parentSelectorName = parentSelectorName; + } + + @Override + public String getChildSelectorName() { + return childSelectorName; + } + + @Override + public String getParentSelectorName() { + return parentSelectorName; + } + + @Override + public String toString() { + return "ISCHILDNODE(" + + quoteSelectorName(childSelectorName) + ", " + + quoteSelectorName(parentSelectorName) + ")"; + } + +} \ No newline at end of file diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/ColumnImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/ColumnImpl.java new file mode 100644 index 00000000000..28efe7a786b --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/ColumnImpl.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.jcr.query.qom; + +import javax.jcr.query.qom.Column; + +/** + * The implementation of the corresponding JCR interface. + */ +public class ColumnImpl extends QOMNode implements Column { + + private final String selectorName, propertyName, columnName; + + public ColumnImpl(String selectorName, String propertyName, String columnName) { + this.selectorName = selectorName; + this.propertyName = propertyName; + this.columnName = columnName; + } + + @Override + public String getColumnName() { + return columnName; + } + + @Override + public String getPropertyName() { + return propertyName; + } + + @Override + public String getSelectorName() { + return selectorName; + } + + @Override + public String toString() { + StringBuilder buff = new StringBuilder(); + if (selectorName != null) { + buff.append(quoteSelectorName(selectorName)); + buff.append('.'); + } + if (propertyName != null) { + buff.append(quotePropertyName(propertyName)); + } else { + buff.append('*'); + } + if (columnName != null) { + buff.append(" AS ").append(quoteColumnName(columnName)); + } + return buff.toString(); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/ComparisonImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/ComparisonImpl.java new file mode 100644 index 00000000000..4a9dff0c11d --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/ComparisonImpl.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.query.qom.Comparison; + +/** + * The implementation of the corresponding JCR interface. + */ +public class ComparisonImpl extends ConstraintImpl implements Comparison { + + private final DynamicOperandImpl operand1; + private final Operator operator; + private final StaticOperandImpl operand2; + + public ComparisonImpl(DynamicOperandImpl operand1, Operator operator, StaticOperandImpl operand2) { + this.operand1 = operand1; + this.operator = operator; + this.operand2 = operand2; + } + + @Override + public DynamicOperandImpl getOperand1() { + return operand1; + } + + @Override + public String getOperator() { + return operator.toString(); + } + + @Override + public StaticOperandImpl getOperand2() { + return operand2; + } + + @Override + public String toString() { + return operator.formatSql(operand1.toString(), operand2.toString()); + } + + @Override + public void bindVariables(QueryObjectModelImpl qom) { + operand2.bindVariables(qom); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/ConstraintImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/ConstraintImpl.java new file mode 100644 index 00000000000..679741973f5 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/ConstraintImpl.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.jcr.query.qom; + +import javax.jcr.query.qom.Constraint; + +/** + * The implementation of the corresponding JCR interface. + */ +public abstract class ConstraintImpl extends QOMNode implements Constraint { + + public abstract void bindVariables(QueryObjectModelImpl qom); + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/DescendantNodeImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/DescendantNodeImpl.java new file mode 100644 index 00000000000..9dbbaf13ba2 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/DescendantNodeImpl.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.query.qom.DescendantNode; + +/** + * The implementation of the corresponding JCR interface. + */ +public class DescendantNodeImpl extends ConstraintImpl implements DescendantNode { + + private final String selectorName; + private final String ancestorPath; + + public DescendantNodeImpl(String selectorName, String ancestorPath) { + this.selectorName = selectorName; + this.ancestorPath = ancestorPath; + } + + @Override + public String getSelectorName() { + return selectorName; + } + + @Override + public String getAncestorPath() { + return ancestorPath; + } + + @Override + public String toString() { + StringBuilder buff = new StringBuilder(); + buff.append("ISDESCENDANTNODE("); + if (selectorName != null) { + buff.append(quoteSelectorName(selectorName)).append(", "); + } + buff.append(quotePath(ancestorPath)).append(')'); + return buff.toString(); + } + + @Override + public void bindVariables(QueryObjectModelImpl qom) { + // ignore + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/DescendantNodeJoinConditionImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/DescendantNodeJoinConditionImpl.java new file mode 100644 index 00000000000..cb05e6254fc --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/DescendantNodeJoinConditionImpl.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.jcr.query.qom; + +import javax.jcr.query.qom.DescendantNodeJoinCondition; + +/** + * The implementation of the corresponding JCR interface. + */ +public class DescendantNodeJoinConditionImpl extends JoinConditionImpl implements + DescendantNodeJoinCondition { + + private final String descendantSelectorName; + private final String ancestorSelectorName; + + public DescendantNodeJoinConditionImpl(String descendantSelectorName, + String ancestorSelectorName) { + this.descendantSelectorName = descendantSelectorName; + this.ancestorSelectorName = ancestorSelectorName; + } + + @Override + public String getDescendantSelectorName() { + return descendantSelectorName; + } + + @Override + public String getAncestorSelectorName() { + return ancestorSelectorName; + } + + @Override + public String toString() { + return "ISDESCENDANTNODE(" + + quoteSelectorName(descendantSelectorName) + ", " + + quoteSelectorName(ancestorSelectorName) + ')'; + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/json/JsopReader.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/DynamicOperandImpl.java similarity index 74% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/json/JsopReader.java rename to oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/DynamicOperandImpl.java index c050b08a0fb..88d0338319c 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/json/JsopReader.java +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/DynamicOperandImpl.java @@ -14,24 +14,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.json; +package org.apache.jackrabbit.oak.jcr.query.qom; -public interface JsopReader { +import javax.jcr.query.qom.DynamicOperand; - String read(int type); - - String readString(); - - int read(); - - boolean matches(int type); - - String readRawValue(); - - String getToken(); - - int getTokenType(); +/** + * The base class for dynamic operands. + */ +public abstract class DynamicOperandImpl extends QOMNode implements DynamicOperand { - void resetReader(); + // base class without methods } diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/EquiJoinConditionImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/EquiJoinConditionImpl.java new file mode 100644 index 00000000000..0b7f0b27fcd --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/EquiJoinConditionImpl.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.jcr.query.qom; + +import javax.jcr.query.qom.EquiJoinCondition; + +/** + * The implementation of the corresponding JCR interface. + */ +public class EquiJoinConditionImpl extends JoinConditionImpl implements EquiJoinCondition { + + private final String property1Name; + private final String property2Name; + private final String selector1Name; + private final String selector2Name; + + public EquiJoinConditionImpl(String selector1Name, String property1Name, String selector2Name, + String property2Name) { + this.selector1Name = selector1Name; + this.property1Name = property1Name; + this.selector2Name = selector2Name; + this.property2Name = property2Name; + } + + @Override + public String getSelector1Name() { + return selector1Name; + } + + @Override + public String getProperty1Name() { + return property1Name; + } + + @Override + public String getSelector2Name() { + return selector2Name; + } + + @Override + public String getProperty2Name() { + return property2Name; + } + + @Override + public String toString() { + return quoteSelectorName(selector1Name) + '.' + + quotePropertyName(property1Name) + " = " + + quoteSelectorName(selector2Name) + '.' + + quotePropertyName(property2Name); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/FullTextSearchImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/FullTextSearchImpl.java new file mode 100644 index 00000000000..38d15732d86 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/FullTextSearchImpl.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.query.qom.FullTextSearch; + +/** + * The implementation of the corresponding JCR interface. + */ +public class FullTextSearchImpl extends ConstraintImpl implements FullTextSearch { + + private final String selectorName; + private final String propertyName; + private final StaticOperandImpl fullTextSearchExpression; + + public FullTextSearchImpl(String selectorName, String propertyName, + StaticOperandImpl fullTextSearchExpression) { + this.selectorName = selectorName; + this.propertyName = propertyName; + this.fullTextSearchExpression = fullTextSearchExpression; + } + + @Override + public StaticOperandImpl getFullTextSearchExpression() { + return fullTextSearchExpression; + } + + @Override + public String getPropertyName() { + return propertyName; + } + + @Override + public String getSelectorName() { + return selectorName; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("CONTAINS("); + if (selectorName != null) { + builder.append(quoteSelectorName(selectorName)); + builder.append('.'); + } + if (propertyName != null) { + builder.append(quotePropertyName(propertyName)); + } else { + builder.append('*'); + } + builder.append(", "); + builder.append(getFullTextSearchExpression()); + builder.append(')'); + return builder.toString(); + } + + @Override + public void bindVariables(QueryObjectModelImpl qom) { + this.fullTextSearchExpression.bindVariables(qom); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/FullTextSearchScoreImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/FullTextSearchScoreImpl.java new file mode 100644 index 00000000000..8ea7862c508 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/FullTextSearchScoreImpl.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.query.qom.FullTextSearchScore; + +/** + * The implementation of the corresponding JCR interface. + */ +public class FullTextSearchScoreImpl extends DynamicOperandImpl implements FullTextSearchScore { + + private final String selectorName; + + public FullTextSearchScoreImpl(String selectorName) { + this.selectorName = selectorName; + } + + @Override + public String getSelectorName() { + return selectorName; + } + + @Override + public String toString() { + if (selectorName == null) { + return "SCORE()"; + } + return "SCORE(" + quoteSelectorName(selectorName) + ')'; + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/JoinConditionImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/JoinConditionImpl.java new file mode 100644 index 00000000000..db36f41208a --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/JoinConditionImpl.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law + * or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.query.qom.JoinCondition; + +/** + * The base class for join conditions. + */ +public abstract class JoinConditionImpl extends QOMNode implements JoinCondition { + + // base class without methods + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/JoinImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/JoinImpl.java new file mode 100644 index 00000000000..cc40bd06299 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/JoinImpl.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law + * or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.query.qom.Join; + +/** + * The implementation of the corresponding JCR interface. + */ +public class JoinImpl extends SourceImpl implements Join { + + private final JoinConditionImpl joinCondition; + private final JoinType joinType; + private final SourceImpl left; + private final SourceImpl right; + + public JoinImpl(SourceImpl left, SourceImpl right, JoinType joinType, + JoinConditionImpl joinCondition) { + this.left = left; + this.right = right; + this.joinType = joinType; + this.joinCondition = joinCondition; + } + + @Override + public JoinConditionImpl getJoinCondition() { + return joinCondition; + } + + @Override + public String getJoinType() { + return joinType.toString(); + } + + @Override + public SourceImpl getLeft() { + return left; + } + + @Override + public SourceImpl getRight() { + return right; + } + + @Override + public String toString() { + return joinType.formatSql(left, right, joinCondition); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/JoinType.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/JoinType.java new file mode 100644 index 00000000000..0d9d3ddf400 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/JoinType.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.RepositoryException; +import javax.jcr.query.qom.Join; +import javax.jcr.query.qom.JoinCondition; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.query.qom.QueryObjectModelFactory; +import javax.jcr.query.qom.Source; + +/** + * Enumeration of the JCR 2.0 join types. + * + * @since Apache Jackrabbit 2.0 + */ +public enum JoinType { + + INNER(QueryObjectModelConstants.JCR_JOIN_TYPE_INNER, "INNER JOIN"), + + LEFT(QueryObjectModelConstants.JCR_JOIN_TYPE_LEFT_OUTER, "LEFT OUTER JOIN"), + + RIGHT(QueryObjectModelConstants.JCR_JOIN_TYPE_RIGHT_OUTER, "RIGHT OUTER JOIN"); + + /** + * JCR name of this join type. + */ + private final String name; + + private final String sql; + + JoinType(String name, String sql) { + this.name = name; + this.sql = sql; + } + + /** + * Returns the join of the given sources. + * + * @param factory factory for creating the join + * @param left left join source + * @param right right join source + * @param condition join condition + * @return join + * @throws RepositoryException if the join can not be created + */ + public Join join( + QueryObjectModelFactory factory, + Source left, Source right, JoinCondition condition) + throws RepositoryException { + return factory.join(left, right, name, condition); + } + + /** + * Formats an SQL join with this join type and the given sources and + * join condition. The sources and condition are simply used as-is, + * without any quoting or escaping. + * + * @param left left source + * @param right right source + * @param condition join condition + * @return SQL join, {@code left join right} + */ + public String formatSql(Object left, Object right, Object condition) { + return left + " " + sql + ' ' + right + " ON " + condition; + } + + /** + * Returns the JCR 2.0 name of this join type. + * + * @see QueryObjectModelConstants + * @return JCR name of this join type + */ + @Override + public String toString() { + return name; + } + + /** + * Returns the join type with the given JCR name. + * + * @param name JCR name of a join type + * @return join type with the given name + */ + public static JoinType getJoinTypeByName(String name) { + for (JoinType type : JoinType.values()) { + if (type.name.equals(name)) { + return type; + } + } + throw new IllegalArgumentException("Unknown join type name: " + name); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/LengthImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/LengthImpl.java new file mode 100644 index 00000000000..7eb80a9ce24 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/LengthImpl.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.query.qom.Length; + +/** + * The implementation of the corresponding JCR interface. + */ +public class LengthImpl extends DynamicOperandImpl implements Length { + + private final PropertyValueImpl propertyValue; + + public LengthImpl(PropertyValueImpl propertyValue) { + this.propertyValue = propertyValue; + } + + @Override + public PropertyValueImpl getPropertyValue() { + return propertyValue; + } + + @Override + public String toString() { + return "LENGTH(" + propertyValue + ')'; + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/LiteralImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/LiteralImpl.java new file mode 100644 index 00000000000..7e2537a117d --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/LiteralImpl.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.query.qom.Literal; + +/** + * The implementation of the corresponding JCR interface. + */ +public class LiteralImpl extends StaticOperandImpl implements Literal { + + private final Value value; + + public LiteralImpl(Value value) { + this.value = value; + } + + @Override + public Value getLiteralValue() { + return value; + } + + @Override + public String toString() { + try { + switch (value.getType()) { + case PropertyType.BINARY: + return cast("BINARY"); + case PropertyType.BOOLEAN: + return cast("BOOLEAN"); + case PropertyType.DATE: + return cast("DATE"); + case PropertyType.DECIMAL: + return cast("DECIMAL"); + case PropertyType.DOUBLE: + case PropertyType.LONG: + return value.getString(); + case PropertyType.NAME: + return cast("NAME"); + case PropertyType.PATH: + return cast("PATH"); + case PropertyType.REFERENCE: + return cast("REFERENCE"); + case PropertyType.STRING: + return escape(value.getString()); + case PropertyType.URI: + return cast("URI"); + case PropertyType.WEAKREFERENCE: + return cast("WEAKREFERENCE"); + default: + return escape(value.getString()); + } + } catch (RepositoryException e) { + return value.toString(); + } + } + + private String cast(String type) throws RepositoryException { + return "CAST(" + escape(value.getString()) + " AS " + type + ')'; + } + + public static final String escape(String v) { + return '\'' + v.replace("'", "''") + '\''; + } + + @Override + public void bindVariables(QueryObjectModelImpl qom) { + // ignore + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/LowerCaseImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/LowerCaseImpl.java new file mode 100644 index 00000000000..685810a681b --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/LowerCaseImpl.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.query.qom.LowerCase; + +/** + * The implementation of the corresponding JCR interface. + */ +public class LowerCaseImpl extends DynamicOperandImpl implements LowerCase { + + private final DynamicOperandImpl operand; + + public LowerCaseImpl(DynamicOperandImpl operand) { + this.operand = operand; + } + + @Override + public DynamicOperandImpl getOperand() { + return operand; + } + + @Override + public String toString() { + return "LOWER(" + operand + ')'; + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/NodeLocalNameImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/NodeLocalNameImpl.java new file mode 100644 index 00000000000..59cadc83a89 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/NodeLocalNameImpl.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.query.qom.NodeLocalName; + +/** + * The implementation of the corresponding JCR interface. + */ +public class NodeLocalNameImpl extends DynamicOperandImpl implements NodeLocalName { + + private final String selectorName; + + public NodeLocalNameImpl(String selectorName) { + this.selectorName = selectorName; + } + + @Override + public String getSelectorName() { + return selectorName; + } + + @Override + public String toString() { + if (selectorName == null) { + return "LOCALNAME()"; + } + return "LOCALNAME(" + quoteSelectorName(selectorName) + ')'; + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/NodeNameImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/NodeNameImpl.java new file mode 100644 index 00000000000..ea64ca294ad --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/NodeNameImpl.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.query.qom.NodeName; + +/** + * The implementation of the corresponding JCR interface. + */ +public class NodeNameImpl extends DynamicOperandImpl implements NodeName { + + private final String selectorName; + + public NodeNameImpl(String selectorName) { + this.selectorName = selectorName; + } + + @Override + public String getSelectorName() { + return selectorName; + } + + @Override + public String toString() { + if (selectorName == null) { + return "NAME()"; + } + return "NAME(" + quoteSelectorName(selectorName) + ')'; + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/NotImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/NotImpl.java new file mode 100644 index 00000000000..ab82b24ca1f --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/NotImpl.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.jcr.query.qom; + +import javax.jcr.query.qom.Not; + +/** + * The implementation of the corresponding JCR interface. + */ +public class NotImpl extends ConstraintImpl implements Not { + + private final ConstraintImpl constraint; + + public NotImpl(ConstraintImpl constraint) { + this.constraint = constraint; + } + + @Override + public ConstraintImpl getConstraint() { + return constraint; + } + + @Override + public String toString() { + return "NOT " + protect(constraint); + } + + @Override + public void bindVariables(QueryObjectModelImpl qom) { + constraint.bindVariables(qom); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/Operator.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/Operator.java new file mode 100644 index 00000000000..4dbfdd9dca0 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/Operator.java @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.RepositoryException; +import javax.jcr.query.qom.Comparison; +import javax.jcr.query.qom.DynamicOperand; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.query.qom.QueryObjectModelFactory; +import javax.jcr.query.qom.StaticOperand; + +/** + * Enumeration of the JCR 2.0 query operators. + * + * @since Apache Jackrabbit 2.0 + */ +public enum Operator { + + EQ(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, "="), + + NE(QueryObjectModelConstants.JCR_OPERATOR_NOT_EQUAL_TO, "!=", "<>"), + + GT(QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN, ">"), + + GE(QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO, ">="), + + LT(QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN, "<"), + + LE(QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO, "<="), + + LIKE(QueryObjectModelConstants.JCR_OPERATOR_LIKE, null, "like"); + + /** + * JCR name of this operator. + */ + private final String name; + + /** + * This operator in XPath syntax. + */ + private final String xpath; + + /** + * This operator in SQL syntax. + */ + private final String sql; + + Operator(String name, String op) { + this(name, op, op); + } + + Operator(String name, String xpath, String sql) { + this.name = name; + this.xpath = xpath; + this.sql = sql; + } + + /** + * Returns a comparison between the given operands using this operator. + * + * @param factory factory for creating the comparison + * @param left operand on the left hand side + * @param right operand on the right hand side + * @return comparison + * @throws RepositoryException if the comparison can not be created + */ + public Comparison comparison( + QueryObjectModelFactory factory, + DynamicOperand left, StaticOperand right) + throws RepositoryException { + return factory.comparison(left, name, right); + } + + /** + * Formats an XPath constraint with this operator and the given operands. + * The operands are simply used as-is, without any quoting or escaping. + * + * @param a first operand + * @param b second operand + * @return XPath constraint, {@code a op b} or + * {@code jcr:like(a, b)} for {@link #LIKE} + */ + public String formatXpath(String a, String b) { + if (this == LIKE) { + return "jcr:like(" + a + ", " + b + ')'; + } + return a + ' ' + xpath + ' ' + b; + } + + /** + * Formats an SQL constraint with this operator and the given operands. + * The operands are simply used as-is, without any quoting or escaping. + * + * @param a first operand + * @param b second operand + * @return SQL constraint, {@code a op b} + */ + public String formatSql(String a, String b) { + return a + ' ' + sql + ' ' + b; + } + + /** + * Returns the JCR 2.0 name of this query operator. + * + * @see QueryObjectModelConstants + * @return JCR name of this operator + */ + @Override + public String toString() { + return name; + } + + /** + * Returns an array of the names of all the JCR 2.0 query operators. + * + * @return names of all query operators + */ + public static String[] getAllQueryOperators() { + return new String[] { + EQ.toString(), + NE.toString(), + GT.toString(), + GE.toString(), + LT.toString(), + LE.toString(), + LIKE.toString() + }; + } + + /** + * Returns the operator with the given JCR name. + * + * @param name JCR name of an operator + * @return operator with the given name + */ + public static Operator getOperatorByName(String name) { + for (Operator operator : Operator.values()) { + if (operator.name.equals(name)) { + return operator; + } + } + throw new IllegalArgumentException("Unknown operator name: " + name); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/OrImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/OrImpl.java new file mode 100644 index 00000000000..8efda9f9158 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/OrImpl.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.jcr.query.qom; + +import javax.jcr.query.qom.Or; + +/** + * The implementation of the corresponding JCR interface. + */ +public class OrImpl extends ConstraintImpl implements Or { + + private final ConstraintImpl constraint1; + private final ConstraintImpl constraint2; + + public OrImpl(ConstraintImpl constraint1, ConstraintImpl constraint2) { + this.constraint1 = constraint1; + this.constraint2 = constraint2; + } + + @Override + public ConstraintImpl getConstraint1() { + return constraint1; + } + + @Override + public ConstraintImpl getConstraint2() { + return constraint2; + } + + @Override + public String toString() { + return protect(constraint1) + " OR " + protect(constraint2); + } + + @Override + public void bindVariables(QueryObjectModelImpl qom) { + constraint1.bindVariables(qom); + constraint2.bindVariables(qom); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/Order.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/Order.java new file mode 100644 index 00000000000..4d9c036ede8 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/Order.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.query.qom.QueryObjectModelConstants; + +/** + * Enumeration of the JCR 2.0 query order. + * + * @since Apache Jackrabbit 2.0 + */ +public enum Order { + + ASCENDING(QueryObjectModelConstants.JCR_ORDER_ASCENDING), + + DESCENDING(QueryObjectModelConstants.JCR_ORDER_DESCENDING); + + /** + * JCR name of this order. + */ + private final String name; + + Order(String name) { + this.name = name; + } + + /** + * @return the JCR name of this order. + */ + public String getName() { + return name; + } + + /** + * Return the order with the given JCR name. + * + * @param name the JCR name of an order. + * @return the order with the given name. + * @throws IllegalArgumentException if {@code name} is not a known JCR + * order name. + */ + public static Order getOrderByName(String name) { + for (Order order : Order.values()) { + if (order.name.equals(name)) { + return order; + } + } + throw new IllegalArgumentException("Unknown order name: " + name); + } +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/OrderingImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/OrderingImpl.java new file mode 100644 index 00000000000..992a308bc16 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/OrderingImpl.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.jcr.query.qom; + +import javax.jcr.query.qom.Ordering; + +/** + * The implementation of the corresponding JCR interface. + */ +public class OrderingImpl extends QOMNode implements Ordering { + + private final DynamicOperandImpl operand; + private final Order order; + + public OrderingImpl(DynamicOperandImpl operand, Order order) { + this.operand = operand; + this.order = order; + } + + @Override + public DynamicOperandImpl getOperand() { + return operand; + } + + @Override + public String getOrder() { + return order.getName(); + } + + @Override + public String toString() { + if (order == Order.ASCENDING) { + return operand + " ASC"; + } + return operand + " DESC"; + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/PropertyExistenceImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/PropertyExistenceImpl.java new file mode 100644 index 00000000000..389b14c653c --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/PropertyExistenceImpl.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.query.qom.PropertyExistence; + +/** + * The implementation of the corresponding JCR interface. + */ +public class PropertyExistenceImpl extends ConstraintImpl implements PropertyExistence { + + private final String selectorName; + private final String propertyName; + + public PropertyExistenceImpl(String selectorName, String propertyName) { + this.selectorName = selectorName; + this.propertyName = propertyName; + } + + @Override + public String getPropertyName() { + return propertyName; + } + + @Override + public String getSelectorName() { + return selectorName; + } + + @Override + public String toString() { + StringBuilder buff = new StringBuilder(); + if (selectorName != null) { + buff.append(quoteSelectorName(selectorName)).append('.'); + } + if (propertyName != null) { + buff.append(quotePropertyName(propertyName)); + } else { + buff.append("*"); + } + buff.append(" IS NOT NULL"); + return buff.toString(); + } + + @Override + public void bindVariables(QueryObjectModelImpl qom) { + // ignore + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/PropertyValueImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/PropertyValueImpl.java new file mode 100644 index 00000000000..d58f7ab5348 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/PropertyValueImpl.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.jcr.query.qom; + +import javax.jcr.query.qom.PropertyValue; + +/** + * The implementation of the corresponding JCR interface. + */ +public class PropertyValueImpl extends DynamicOperandImpl implements PropertyValue { + + private final String selectorName; + private final String propertyName; + + public PropertyValueImpl(String selectorName, String propertyName) { + this.selectorName = selectorName; + this.propertyName = propertyName; + } + + @Override + public String getSelectorName() { + return selectorName; + } + + @Override + public String getPropertyName() { + return propertyName; + } + + @Override + public String toString() { + StringBuilder buff = new StringBuilder(); + if (selectorName != null) { + buff.append(quoteSelectorName(selectorName)).append('.'); + } + if (propertyName != null) { + buff.append(quotePropertyName(propertyName)); + } else { + buff.append("*"); + } + return buff.toString(); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/QOMNode.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/QOMNode.java new file mode 100644 index 00000000000..7da28edc300 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/QOMNode.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.jcr.query.qom; + +/** + * The base class for all QOM nodes. + */ +abstract class QOMNode { + + protected String protect(Object expression) { + String str = expression.toString(); + if (str.indexOf(' ') >= 0) { + return '(' + str + ')'; + } + return str; + } + + protected String quotePath(String path) { + return quoteName(path); + } + + protected String quoteSelectorName(String name) { + return quoteName(name); + } + + protected String quotePropertyName(String name) { + return quoteName(name); + } + + protected String quoteColumnName(String name) { + return quoteName(name); + } + + protected String quoteNodeTypeName(String name) { + return quoteName(name); + } + + private static String quoteName(String name) { + return '[' + name + ']'; + } + +} + diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/QueryObjectModelFactoryImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/QueryObjectModelFactoryImpl.java new file mode 100644 index 00000000000..3c592afcb27 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/QueryObjectModelFactoryImpl.java @@ -0,0 +1,234 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law + * or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.query.qom.ChildNode; +import javax.jcr.query.qom.ChildNodeJoinCondition; +import javax.jcr.query.qom.Column; +import javax.jcr.query.qom.Comparison; +import javax.jcr.query.qom.Constraint; +import javax.jcr.query.qom.DescendantNode; +import javax.jcr.query.qom.DescendantNodeJoinCondition; +import javax.jcr.query.qom.DynamicOperand; +import javax.jcr.query.qom.EquiJoinCondition; +import javax.jcr.query.qom.FullTextSearch; +import javax.jcr.query.qom.FullTextSearchScore; +import javax.jcr.query.qom.Join; +import javax.jcr.query.qom.JoinCondition; +import javax.jcr.query.qom.Length; +import javax.jcr.query.qom.Literal; +import javax.jcr.query.qom.LowerCase; +import javax.jcr.query.qom.NodeLocalName; +import javax.jcr.query.qom.NodeName; +import javax.jcr.query.qom.Not; +import javax.jcr.query.qom.Or; +import javax.jcr.query.qom.Ordering; +import javax.jcr.query.qom.PropertyExistence; +import javax.jcr.query.qom.PropertyValue; +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.query.qom.QueryObjectModelFactory; +import javax.jcr.query.qom.SameNode; +import javax.jcr.query.qom.SameNodeJoinCondition; +import javax.jcr.query.qom.Selector; +import javax.jcr.query.qom.Source; +import javax.jcr.query.qom.StaticOperand; +import javax.jcr.query.qom.UpperCase; +import org.apache.jackrabbit.oak.jcr.query.QueryManagerImpl; + +/** + * The implementation of the corresponding JCR interface. + */ +public class QueryObjectModelFactoryImpl implements QueryObjectModelFactory { + + private final QueryManagerImpl queryManager; + private final ValueFactory valueFactory; + + public QueryObjectModelFactoryImpl(QueryManagerImpl queryManager, ValueFactory valueFactory) { + this.queryManager = queryManager; + this.valueFactory = valueFactory; + } + + @Override + public AndImpl and(Constraint constraint1, Constraint constraint2) { + return new AndImpl((ConstraintImpl) constraint1, (ConstraintImpl) constraint2); + } + + @Override + public OrderingImpl ascending(DynamicOperand operand) { + return new OrderingImpl((DynamicOperandImpl) operand, Order.ASCENDING); + } + + @Override + public BindVariableValueImpl bindVariable(String bindVariableName) { + return new BindVariableValueImpl(bindVariableName); + } + + @Override + public ChildNode childNode(String selectorName, String path) { + return new ChildNodeImpl(selectorName, path); + } + + @Override + public ChildNodeJoinCondition childNodeJoinCondition( + String childSelectorName, String parentSelectorName) { + return new ChildNodeJoinConditionImpl(childSelectorName, parentSelectorName); + } + + @Override + public Column column(String selectorName, + String propertyName, String columnName) throws RepositoryException { + return new ColumnImpl(selectorName, getOakName(propertyName), columnName); + } + + @Override + public Comparison comparison(DynamicOperand operand1, + String operator, StaticOperand operand2) { + return new ComparisonImpl((DynamicOperandImpl) operand1, + Operator.getOperatorByName(operator), (StaticOperandImpl) operand2); + } + + @Override + public DescendantNode descendantNode(String selectorName, String path) { + return new DescendantNodeImpl(selectorName, path); + } + + @Override + public DescendantNodeJoinCondition descendantNodeJoinCondition( + String descendantSelectorName, + String ancestorSelectorName) { + return new DescendantNodeJoinConditionImpl( + descendantSelectorName, ancestorSelectorName); + } + + @Override + public Ordering descending(DynamicOperand operand) { + return new OrderingImpl((DynamicOperandImpl) operand, Order.DESCENDING); + } + + @Override + public EquiJoinCondition equiJoinCondition( + String selector1Name, String property1Name, + String selector2Name, String property2Name) throws RepositoryException { + return new EquiJoinConditionImpl(selector1Name, getOakName(property1Name), + selector2Name, getOakName(property2Name)); + } + + @Override + public FullTextSearch fullTextSearch(String selectorName, String propertyName, + StaticOperand fullTextSearchExpression) throws RepositoryException { + return new FullTextSearchImpl(selectorName, getOakName(propertyName), + (StaticOperandImpl) fullTextSearchExpression); + } + + @Override + public FullTextSearchScore fullTextSearchScore(String selectorName) { + return new FullTextSearchScoreImpl(selectorName); + } + + @Override + public Join join(Source left, Source right, String joinType, JoinCondition joinCondition) { + return new JoinImpl((SourceImpl) left, (SourceImpl) right, + JoinType.getJoinTypeByName(joinType), (JoinConditionImpl) joinCondition); + } + + @Override + public Length length(PropertyValue propertyValue) { + return new LengthImpl((PropertyValueImpl) propertyValue); + } + + @Override + public Literal literal(Value literalValue) { + return new LiteralImpl(literalValue); + } + + @Override + public LowerCase lowerCase(DynamicOperand operand) { + return new LowerCaseImpl((DynamicOperandImpl) operand); + } + + @Override + public NodeLocalName nodeLocalName(String selectorName) { + return new NodeLocalNameImpl(selectorName); + } + + @Override + public NodeName nodeName(String selectorName) { + return new NodeNameImpl(selectorName); + } + + @Override + public Not not(Constraint constraint) { + return new NotImpl((ConstraintImpl) constraint); + } + + @Override + public Or or(Constraint constraint1, Constraint constraint2) { + return new OrImpl((ConstraintImpl) constraint1, (ConstraintImpl) constraint2); + } + + @Override + public PropertyExistence propertyExistence(String selectorName, + String propertyName) throws RepositoryException { + return new PropertyExistenceImpl(selectorName, + getOakName(propertyName)); + } + + @Override + public PropertyValue propertyValue(String selectorName, + String propertyName) throws RepositoryException { + return new PropertyValueImpl(selectorName, getOakName(propertyName)); + } + + @Override + public SameNode sameNode(String selectorName, String path) { + return new SameNodeImpl(selectorName, path); + } + + @Override + public SameNodeJoinCondition sameNodeJoinCondition(String selector1Name, + String selector2Name, String selector2Path) { + return new SameNodeJoinConditionImpl(selector1Name, selector2Name, selector2Path); + } + + @Override + public Selector selector(String nodeTypeName, String selectorName) + throws RepositoryException { + return new SelectorImpl(getOakName(nodeTypeName), selectorName); + } + + @Override + public UpperCase upperCase(DynamicOperand operand) { + return new UpperCaseImpl((DynamicOperandImpl) operand); + } + + @Override + public QueryObjectModel createQuery(Source source, Constraint constraint, + Ordering[] orderings, Column[] columns) { + QueryObjectModelImpl qom = new QueryObjectModelImpl(queryManager, + valueFactory, source, constraint, orderings, columns); + qom.bindVariables(); + return qom; + } + + private String getOakName(String jcrName) throws RepositoryException { + if (jcrName == null) { + return null; + } + return queryManager.getSessionDelegate().getOakNameOrThrow(jcrName); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/QueryObjectModelImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/QueryObjectModelImpl.java new file mode 100644 index 00000000000..e374c8c0d56 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/QueryObjectModelImpl.java @@ -0,0 +1,199 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law + * or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import java.util.HashMap; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import javax.jcr.query.qom.Column; +import javax.jcr.query.qom.Constraint; +import javax.jcr.query.qom.Ordering; +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.query.qom.Source; +import org.apache.jackrabbit.oak.jcr.query.QueryManagerImpl; + +/** + * The implementation of the corresponding JCR interface. + */ +public class QueryObjectModelImpl implements QueryObjectModel { + + private final Source source; + private final Constraint constraint; + private final HashMap bindVariableMap = new HashMap(); + private final QueryManagerImpl queryManager; + private final ValueFactory valueFactory; + private final Ordering[] orderings; + private final Column[] columns; + private long limit = Long.MAX_VALUE; + private long offset; + private boolean parsed; + private String storedQueryPath; + + public QueryObjectModelImpl(QueryManagerImpl queryManager, + ValueFactory valueFactory, Source source, Constraint constraint, + Ordering[] orderings, Column[] columns) { + this.queryManager = queryManager; + this.valueFactory = valueFactory; + this.source = source; + this.constraint = constraint; + this.orderings = orderings; + this.columns = columns; + } + + public void bindVariables() { + if (constraint != null) { + ((ConstraintImpl) constraint).bindVariables(this); + } + } + + @Override + public Column[] getColumns() { + return columns == null ? new Column[0] : columns; + } + + @Override + public Constraint getConstraint() { + return constraint; + } + + @Override + public Ordering[] getOrderings() { + return orderings == null ? new Ordering[0] : orderings; + } + + @Override + public Source getSource() { + return source; + } + + @Override + public String[] getBindVariableNames() throws RepositoryException { + parse(); + String[] names = new String[bindVariableMap.size()]; + bindVariableMap.keySet().toArray(names); + return names; + } + + @Override + public void setLimit(long limit) { + this.limit = limit; + } + + @Override + public void setOffset(long offset) { + this.offset = offset; + } + + public ValueFactory getValueFactory() { + return valueFactory; + } + + @Override + public void bindValue(String varName, Value value) throws RepositoryException { + parse(); + if (!bindVariableMap.containsKey(varName)) { + throw new IllegalArgumentException("Variable name " + varName + + " is not a valid variable in this query"); + } + bindVariableMap.put(varName, value); + } + + private void parse() throws RepositoryException { + if (parsed) { + return; + } + String[] names = queryManager.createQuery(getStatement(), Query.JCR_SQL2). + getBindVariableNames(); + for (String n : names) { + bindVariableMap.put(n, null); + } + parsed = true; + } + + @Override + public QueryResult execute() throws RepositoryException { + return queryManager.executeQuery(getStatement(), Query.JCR_SQL2, + limit, offset, bindVariableMap); + } + + @Override + public String getLanguage() { + return Query.JCR_JQOM; + } + + @Override + public String getStatement() { + StringBuilder buff = new StringBuilder(); + buff.append("select "); + int i; + if (columns != null && columns.length > 0) { + i = 0; + for (Column c : columns) { + if (i++ > 0) { + buff.append(", "); + } + buff.append(c); + } + } else { + buff.append("*"); + } + buff.append(" from "); + buff.append(source); + if (constraint != null) { + buff.append(" where "); + buff.append(constraint); + } + if (orderings != null && orderings.length > 0) { + buff.append(" order by "); + i = 0; + for (Ordering o : orderings) { + if (i++ > 0) { + buff.append(", "); + } + buff.append(o); + } + } + return buff.toString(); + } + + @Override + public String getStoredQueryPath() throws RepositoryException { + if (storedQueryPath == null) { + throw new ItemNotFoundException("Not a stored query"); + } + return storedQueryPath; + } + + @Override + public Node storeAsNode(String absPath) throws RepositoryException { + Node n = queryManager.createQuery(getStatement(), Query.JCR_JQOM). + storeAsNode(absPath); + storedQueryPath = n.getPath(); + return n; + } + + public void addBindVariable(BindVariableValueImpl var) { + this.bindVariableMap.put(var.getBindVariableName(), null); + } + + public String toString() { + return getStatement(); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/SameNodeImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/SameNodeImpl.java new file mode 100644 index 00000000000..19e6d51b25d --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/SameNodeImpl.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.query.qom.SameNode; + +/** + * The implementation of the corresponding JCR interface. + */ +public class SameNodeImpl extends ConstraintImpl implements SameNode { + + private final String path; + private final String selectorName; + + public SameNodeImpl(String selectorName, String path) { + this.selectorName = selectorName; + this.path = path; + } + + @Override + public String getSelectorName() { + return selectorName; + } + + @Override + public String getPath() { + return path; + } + + @Override + public String toString() { + StringBuilder buff = new StringBuilder(); + buff.append("ISSAMENODE("); + if (selectorName != null) { + buff.append(quoteSelectorName(selectorName)).append(", "); + } + buff.append(quotePath(path)).append(')'); + return buff.toString(); + } + + @Override + public void bindVariables(QueryObjectModelImpl qom) { + // ignore + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/SameNodeJoinConditionImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/SameNodeJoinConditionImpl.java new file mode 100644 index 00000000000..ba0e814fa4b --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/SameNodeJoinConditionImpl.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.jcr.query.qom; + +import javax.jcr.query.qom.SameNodeJoinCondition; + +/** + * The implementation of the corresponding JCR interface. + */ +public class SameNodeJoinConditionImpl extends JoinConditionImpl implements SameNodeJoinCondition { + + private final String selector1Name; + private final String selector2Name; + private final String selector2Path; + + public SameNodeJoinConditionImpl(String selector1Name, String selector2Name, + String selector2Path) { + this.selector1Name = selector1Name; + this.selector2Name = selector2Name; + this.selector2Path = selector2Path; + } + + @Override + public String getSelector1Name() { + return selector1Name; + } + + @Override + public String getSelector2Name() { + return selector2Name; + } + + @Override + public String getSelector2Path() { + return selector2Path; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("ISSAMENODE("); + builder.append(quoteSelectorName(selector1Name)); + builder.append(", "); + builder.append(quoteSelectorName(selector2Name)); + if (selector2Path != null) { + builder.append(", "); + builder.append(quotePath(selector2Path)); + } + builder.append(')'); + return builder.toString(); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/SelectorImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/SelectorImpl.java new file mode 100644 index 00000000000..ba13c53d46a --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/SelectorImpl.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.jcr.query.qom; + +import javax.jcr.query.qom.Selector; + +/** + * The implementation of the corresponding JCR interface. + */ +public class SelectorImpl extends SourceImpl implements Selector { + + private final String nodeTypeName, selectorName; + + public SelectorImpl(String nodeTypeName, String selectorName) { + this.nodeTypeName = nodeTypeName; + this.selectorName = selectorName; + } + + @Override + public String getNodeTypeName() { + return nodeTypeName; + } + + @Override + public String getSelectorName() { + return selectorName; + } + + @Override + public String toString() { + StringBuilder buff = new StringBuilder(); + buff.append(quoteNodeTypeName(nodeTypeName)); + if (selectorName != null) { + buff.append(" AS ").append(quoteSelectorName(selectorName)); + } + return buff.toString(); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/SourceImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/SourceImpl.java new file mode 100644 index 00000000000..d4248751340 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/SourceImpl.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.jcr.query.qom; + +import javax.jcr.query.qom.Source; + +/** + * The base class for sources. + */ +public abstract class SourceImpl extends QOMNode implements Source { + + // base class without methods + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/StaticOperandImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/StaticOperandImpl.java new file mode 100644 index 00000000000..687a822e725 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/StaticOperandImpl.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.jcr.query.qom; + +import javax.jcr.query.qom.StaticOperand; + +/** + * The base class for static operands. + */ +public abstract class StaticOperandImpl extends QOMNode implements StaticOperand { + + public abstract void bindVariables(QueryObjectModelImpl qom); + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/UpperCaseImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/UpperCaseImpl.java new file mode 100644 index 00000000000..902fa64db1f --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/query/qom/UpperCaseImpl.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.query.qom.UpperCase; + +/** + * The implementation of the corresponding JCR interface. + */ +public class UpperCaseImpl extends DynamicOperandImpl implements UpperCase { + + private final DynamicOperandImpl operand; + + public UpperCaseImpl(DynamicOperandImpl operand) { + this.operand = operand; + } + + @Override + public DynamicOperandImpl getOperand() { + return operand; + } + + @Override + public String toString() { + return "UPPER(" + operand + ')'; + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/version/VersionImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/version/VersionImpl.java new file mode 100644 index 00000000000..3e1059f0933 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/version/VersionImpl.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.version; + +import java.util.Calendar; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; + +import org.apache.jackrabbit.oak.jcr.NodeDelegate; +import org.apache.jackrabbit.oak.jcr.NodeImpl; +import org.apache.jackrabbit.oak.util.TODO; + +class VersionImpl extends NodeImpl implements Version { + + public VersionImpl(NodeDelegate dlg) { + super(dlg); + } + + @Override + public VersionHistory getContainingHistory() throws RepositoryException { + return TODO.unimplemented().returnValue(null); + } + + @Override + public Calendar getCreated() throws RepositoryException { + return TODO.unimplemented().returnValue(Calendar.getInstance()); + } + + @Override + public Version getLinearPredecessor() throws RepositoryException { + return TODO.unimplemented().returnValue(null); + } + + @Override + public Version getLinearSuccessor() throws RepositoryException { + return TODO.unimplemented().returnValue(null); + } + + @Override + public Version[] getPredecessors() throws RepositoryException { + return TODO.unimplemented().returnValue(new Version[0]); + } + + @Override + public Version[] getSuccessors() throws RepositoryException { + return TODO.unimplemented().returnValue(new Version[0]); + } + + @Override + public Node getFrozenNode() throws RepositoryException { + return TODO.unimplemented().returnValue(null); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/version/VersionManagerImpl.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/version/VersionManagerImpl.java new file mode 100644 index 00000000000..8517700b5a0 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/version/VersionManagerImpl.java @@ -0,0 +1,171 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.version; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemExistsException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.lock.LockException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionException; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionManager; + +import org.apache.jackrabbit.commons.iterator.NodeIteratorAdapter; +import org.apache.jackrabbit.oak.jcr.NodeDelegate; +import org.apache.jackrabbit.oak.jcr.SessionDelegate; +import org.apache.jackrabbit.oak.util.TODO; + +public class VersionManagerImpl implements VersionManager { + + private final SessionDelegate sessionDelegate; + + public VersionManagerImpl(SessionDelegate sessionDelegate) { + this.sessionDelegate = sessionDelegate; + } + + @Override + public Node setActivity(Node activity) throws RepositoryException { + return TODO.unimplemented().returnValue(null); + } + + @Override + public void restoreByLabel( + String absPath, String versionLabel, boolean removeExisting) + throws RepositoryException { + TODO.unimplemented().doNothing(); + } + + @Override + public void restore( + String absPath, Version version, boolean removeExisting) + throws RepositoryException { + TODO.unimplemented().doNothing(); + } + + @Override + public void restore( + String absPath, String versionName, boolean removeExisting) + throws RepositoryException { + TODO.unimplemented().doNothing(); + } + + @Override + public void restore(Version version, boolean removeExisting) + throws RepositoryException { + TODO.unimplemented().doNothing(); + } + + @Override + public void restore(Version[] versions, boolean removeExisting) + throws ItemExistsException, + UnsupportedRepositoryOperationException, VersionException, + LockException, InvalidItemStateException, RepositoryException { + TODO.unimplemented().doNothing(); + } + + @Override + public void removeActivity(Node activityNode) + throws RepositoryException { + TODO.unimplemented().doNothing(); + } + + @Override + public NodeIterator merge( + String absPath, String srcWorkspace, + boolean bestEffort, boolean isShallow) + throws RepositoryException { + return TODO.unimplemented().returnValue(NodeIteratorAdapter.EMPTY); + } + + @Override + public NodeIterator merge( + String absPath, String srcWorkspace, boolean bestEffort) + throws RepositoryException { + return TODO.unimplemented().returnValue(NodeIteratorAdapter.EMPTY); + } + + @Override + public NodeIterator merge(Node activityNode) throws RepositoryException { + return TODO.unimplemented().returnValue(NodeIteratorAdapter.EMPTY); + } + + @Override + public boolean isCheckedOut(String absPath) throws RepositoryException { + return TODO.unimplemented().returnValue(true); + } + + @Override + public VersionHistory getVersionHistory(String absPath) + throws RepositoryException { + return TODO.unimplemented().returnValue(null); + } + + @Override + public Version getBaseVersion(String absPath) throws RepositoryException { + return TODO.unimplemented().returnValue(null); + } + + @Override + public Node getActivity() throws RepositoryException { + return TODO.unimplemented().returnValue(null); + } + + @Override + public void doneMerge(String absPath, Version version) + throws RepositoryException { + TODO.unimplemented().doNothing(); + } + + @Override + public Node createConfiguration(String absPath) throws RepositoryException { + return TODO.unimplemented().returnValue(null); + } + + @Override + public Node createActivity(String title) throws RepositoryException { + return TODO.unimplemented().returnValue(null); + } + + @Override + public Version checkpoint(String absPath) throws RepositoryException { + return TODO.unimplemented().returnValue(null); + } + + @Override + public void checkout(String absPath) throws RepositoryException { + TODO.unimplemented().doNothing(); + } + + @Override + public Version checkin(String absPath) throws RepositoryException { + String oakPath = sessionDelegate.getOakPathOrThrowNotFound(absPath); + NodeDelegate nodeDelegate = sessionDelegate.getNode(oakPath); + return TODO.dummyImplementation().returnValue( + new VersionImpl(nodeDelegate)); + } + + @Override + public void cancelMerge(String absPath, Version version) + throws RepositoryException { + TODO.unimplemented().doNothing(); + } + +} diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/xml/DocumentViewHandler.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/xml/DocumentViewHandler.java new file mode 100644 index 00000000000..25c67d16fac --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/xml/DocumentViewHandler.java @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.xml; + +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.NamespaceRegistry; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.commons.JcrUtils; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +public class DocumentViewHandler extends DefaultHandler { + + private static class Context { + + private final Context parent; + + private final Node node; + + private final Map namespaces = + new HashMap(); + + private final StringBuilder text = new StringBuilder(); + + public Context(Context parent, Node node) { + this.parent = parent; + this.node = node; + } + + public String getNamespaceURI(String prefix) + throws RepositoryException { + String uri = namespaces.get(prefix); + if (uri != null) { + return uri; + } else if (parent != null) { + return parent.getNamespaceURI(prefix); + } else { + return node.getSession().getNamespacePrefix(uri); + } + } + + public String toJcrName(String qname) + throws RepositoryException { + if (qname != null) { + int colon = qname.indexOf(':'); + if (colon != -1) { + String prefix = qname.substring(0, colon); + String local = qname.substring(colon + 1); + String uri = getNamespaceURI(prefix); + return toJcrName(uri, local, qname); + } else { + return qname; + } + } else { + return null; + } + } + + public String toJcrName(String uri, String local, String qname) + throws RepositoryException { + if (uri != null) { + String prefix = node.getSession().getNamespacePrefix(uri); + if (prefix.isEmpty()) { + return local; + } else { + return prefix + ":" + local; + } + } else if (local != null) { + return local; + } else { + return toJcrName(qname); + } + } + + } + + private Context context; + + public DocumentViewHandler(Node parent, int uuidBehavior) { + this.context = new Context(null, parent); + } + + @Override + public void startElement( + String uri, String localName, String qName, + Attributes atts) throws SAXException { + try { + String name = context.toJcrName(uri, localName, qName); + String type = context.toJcrName(atts.getValue( + NamespaceRegistry.NAMESPACE_JCR, + Property.JCR_PRIMARY_TYPE)); + Node node = JcrUtils.getOrAddNode(context.node, name, type); + for (int i = 0; i < atts.getLength(); i++) { + name = context.toJcrName( + atts.getURI(i), atts.getLocalName(i), atts.getQName(i)); + if (name.equals("jcr:primaryType")) { + type = context.toJcrName(atts.getValue(i)); + if (!node.isNodeType(type)) { + node.setPrimaryType(type); + } + } else if (name.equals("jcr:mixinTypes")) { + String[] types = atts.getValue(i).split("\\s+"); + for (int j = 0; j < types.length; j++) { + type = context.toJcrName(types[i]); + if (!node.isNodeType(type)) { + node.addMixin(type); + } + } + } else { + node.setProperty(name, atts.getValue(i)); + } + } + + context = new Context(context, node); + } catch (RepositoryException e) { + throw new SAXException(e); + } + } + + @Override + public void endElement( + String uri, String localName, String qName) + throws SAXException { + try { + if (context.text.length() > 0) { + Node xmltext = JcrUtils.getOrAddNode(context.node, "jcr:xmltext"); + xmltext.setProperty("jcr:xmlcharacters", context.text.toString()); + } + + context = context.parent; + } catch (RepositoryException e) { + throw new SAXException(e); + } + } + + @Override + public void characters(char[] ch, int offset, int length) { + context.text.append(ch, offset, length); + } + + @Override + public void ignorableWhitespace(char[] ch, int offset, int length) { + context.text.append(ch, offset, length); + } + + @Override + public void startPrefixMapping(String prefix, String uri) + throws SAXException { + context.namespaces.put(prefix, uri); + } + + @Override + public void endPrefixMapping(String prefix) throws SAXException { + context.namespaces.remove(prefix); + } + +} \ No newline at end of file diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/xml/XmlImportHandler.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/xml/XmlImportHandler.java new file mode 100644 index 00000000000..f23a00e9700 --- /dev/null +++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/xml/XmlImportHandler.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.xml; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +public class XmlImportHandler extends DefaultHandler { + + private Node node; + + private String nodeName; + + private String name; + + private final List values = new ArrayList(); + + private final StringBuilder builder = new StringBuilder(); + + public XmlImportHandler(Node parent, int uuidBehavior) { + this.node = parent; + } + + @Override + public void startElement( + String uri, String localName, String qName, + Attributes atts) throws SAXException { + if ("http://www.jcp.org/jcr/sv/1.0".equals(uri)) { + String value = atts.getValue("sv:name"); + if ("node".equals(localName)) { + // create node on jcr:primaryType sv:property + nodeName = value; + } else if (value != null) { + name = value; + } + builder.setLength(0); + } else { + for (int i = 0; i < atts.getLength(); i++) { + try { + node.setProperty(atts.getQName(i), atts.getValue(i)); + } catch (RepositoryException e) { + throw new SAXException(e); + } + } + } + } + + @Override + public void endElement( + String uri, String localName, String qName) + throws SAXException { + if (!"http://www.jcp.org/jcr/sv/1.0".equals(uri)) { + return; + } else if ("value".equals(localName)) { + values.add(builder.toString()); + } else if ("property".equals(localName)) { + try { + if (values.size() == 1) { + if (name.equals("jcr:primaryType")) { + try { + node = node.addNode(nodeName, values.get(0)); + } catch (RepositoryException e) { + throw new SAXException(e); + } + } else if (name.equals("jcr:mixinTypes")) { + node.addMixin(values.get(0)); + } else { + node.setProperty(name, values.get(0)); + } + } else if (name.equals("jcr:mixinTypes")) { + for (String value : values) { + node.addMixin(value); + } + } else { + node.setProperty(name, values.toArray(new String[values.size()])); + } + } catch (RepositoryException e) { + throw new SAXException(e); + } + values.clear(); + } else if ("node".equals(localName)) { + try { + node = node.getParent(); + } catch (RepositoryException e) { + throw new SAXException(e); + } + } + } + + @Override + public void characters(char[] ch, int offset, int length) { + builder.append(ch, offset, length); + } + +} \ No newline at end of file diff --git a/oak-jcr/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory b/oak-jcr/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory new file mode 100644 index 00000000000..0f9d8d71055 --- /dev/null +++ b/oak-jcr/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +org.apache.jackrabbit.oak.jcr.OakRepositoryFactory diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AbstractRepositoryTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AbstractRepositoryTest.java new file mode 100644 index 00000000000..d820dae6e10 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AbstractRepositoryTest.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import javax.jcr.GuestCredentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.security.auth.login.Configuration; + +import org.apache.jackrabbit.oak.security.OakConfiguration; +import org.junit.After; +import org.junit.Before; + +/** + * Abstract base class for repository tests providing methods for accessing + * the repository, a session and nodes and properties from that session. + * + * Users of this class must call clear to close the session associated with + * this instance and clean up the repository when done. + */ +public abstract class AbstractRepositoryTest { + + protected ScheduledExecutorService executor = null; + + private Repository repository = null; + private Session adminSession = null; + + @Before + public void before() throws Exception { + // TODO: OAK-17. workaround for missing test configuration + Configuration.setConfiguration(new OakConfiguration()); + } + + @After + public void logout() throws RepositoryException { + // release session field + if (adminSession != null) { + adminSession.logout(); + adminSession = null; + } + // release repository field + repository = null; + + if (executor != null) { + executor.shutdown(); + executor = null; + } + } + + protected Repository getRepository() throws RepositoryException { + if (repository == null) { + repository = new Jcr().with(getExecutor()).createRepository(); + } + return repository; + } + + private ScheduledExecutorService getExecutor() { + return (executor == null) ? Executors.newScheduledThreadPool(0) : executor; + } + + protected Session getAdminSession() throws RepositoryException { + if (adminSession == null) { + adminSession = createAdminSession(); + } + return adminSession; + } + + protected Session createAnonymousSession() throws RepositoryException { + return getRepository().login(new GuestCredentials()); + } + + protected Session createAdminSession() throws RepositoryException { + return getRepository().login(new SimpleCredentials("admin", "admin".toCharArray())); + } + +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AutoCreatedItemsTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AutoCreatedItemsTest.java new file mode 100644 index 00000000000..1f9335eb66e --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AutoCreatedItemsTest.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.jcr; + +import javax.jcr.Node; +import javax.jcr.Session; +import javax.jcr.Value; + +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * AutoCreatedItemsTest checks if auto-created nodes and properties + * are added correctly as defined in the node type definition. + */ +public class AutoCreatedItemsTest extends AbstractRepositoryTest { + + @Test + public void autoCreatedItems() throws Exception { + Session s = getAdminSession(); + new TestContentLoader().loadTestContent(s); + Node test = s.getRootNode().addNode("test", "test:autoCreate"); + assertTrue(test.hasProperty("test:property")); + assertEquals("default value", test.getProperty("test:property").getString()); + assertTrue(test.hasProperty("test:propertyMulti")); + assertArrayEquals(new Value[]{s.getValueFactory().createValue("value1"), + s.getValueFactory().createValue("value2")}, + test.getProperty("test:propertyMulti").getValues()); + + assertTrue(test.hasNode("test:folder")); + Node folder = test.getNode("test:folder"); + assertEquals("nt:folder", folder.getPrimaryNodeType().getName()); + assertTrue(folder.hasProperty("jcr:created")); + assertTrue(folder.hasProperty("jcr:createdBy")); + } +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/CRUDTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/CRUDTest.java new file mode 100644 index 00000000000..0b48d4b970e --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/CRUDTest.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.fail; + +public class CRUDTest extends AbstractRepositoryTest { + + @Test + public void testCRUD() throws RepositoryException { + Session session = getAdminSession(); + // Create + Node hello = session.getRootNode().addNode("hello"); + hello.setProperty("world", "hello world"); + session.save(); + + // Read + assertEquals( + "hello world", + session.getProperty("/hello/world").getString()); + + // Update + session.getNode("/hello").setProperty("world", "Hello, World!"); + session.save(); + assertEquals( + "Hello, World!", + session.getProperty("/hello/world").getString()); + + // Delete + session.getNode("/hello").remove(); + session.save(); + assertTrue(!session.propertyExists("/hello/world")); + } + + @Test + public void testRemoveBySetProperty() throws RepositoryException { + Session session = getAdminSession(); + Node root = session.getRootNode(); + try { + root.setProperty("test", "abc"); + assertNotNull(root.setProperty("test", (String) null)); + } catch (PathNotFoundException e) { + // success + } + } + + @Test + public void testRemoveBySetMVProperty() throws RepositoryException { + Session session = getAdminSession(); + Node root = session.getRootNode(); + try { + root.setProperty("test", new String[] {"abc", "def"}); + assertNotNull(root.setProperty("test", (String[]) null)); + } catch (PathNotFoundException e) { + // success + } + } + + @Test + public void testRemoveMissingProperty() throws RepositoryException { + Session session = getAdminSession(); + Node root = session.getRootNode(); + Property p = root.setProperty("missing", (String) null); + assertNotNull(p); + try { + p.getValue(); + fail("must throw InvalidItemStateException"); + } catch (InvalidItemStateException e) { + // expected + } + } + + @Test + public void testRemoveMissingMVProperty() throws RepositoryException { + Session session = getAdminSession(); + Node root = session.getRootNode(); + Property p = root.setProperty("missing", (String[]) null); + assertNotNull(p); + try { + p.getValues(); + fail("must throw InvalidItemStateException"); + } catch (InvalidItemStateException e) { + // expected + } + } + + @Test + public void testRootPropertyPath() throws RepositoryException { + Property property = getAdminSession().getRootNode().getProperty("jcr:primaryType"); + assertEquals("/jcr:primaryType", property.getPath()); + } +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/CompatibilityIssuesTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/CompatibilityIssuesTest.java new file mode 100644 index 00000000000..231b9070255 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/CompatibilityIssuesTest.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.oak.Oak; +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.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * This class contains test cases which demonstrate changes in behaviour wrt. to Jackrabbit 2. + * See OAK-14: Identify and document changes in behaviour wrt. Jackrabbit 2 + */ +public class CompatibilityIssuesTest extends AbstractRepositoryTest { + + /** + * Trans-session isolation differs from Jackrabbit 2. Snapshot isolation can + * result in write skew as this test demonstrates: the check method enforces + * an application logic constraint which says that the sum of the properties + * p1 and p2 must not be negative. While session1 and session2 each enforce + * this constraint before saving, the constraint might not hold globally as + * can be seen in session3. + * + * @see + * Transactional model of the Microkernel based Jackrabbit prototype + */ + @Test + public void sessionIsolation() throws RepositoryException { + Session session0 = createAdminSession(); + Session session1 = null; + Session session2 = null; + try { + Node testNode = session0.getNode("/").addNode("testNode"); + testNode.setProperty("p1", 1); + testNode.setProperty("p2", 1); + session0.save(); + check(getAdminSession()); + + session1 = createAdminSession(); + session2 = createAdminSession(); + session1.getNode("/testNode").setProperty("p1", -1); + check(session1); + session1.save(); + + session2.getNode("/testNode").setProperty("p2", -1); + check(session2); // Throws on JR2, not on Oak + session2.save(); + + Session session3 = createAnonymousSession(); + try { + check(session3); // Throws on Oak + fail(); + } catch (AssertionError e) { + // expected + } finally { + session3.logout(); + } + + } finally { + session0.logout(); + if (session1 != null) { + session1.logout(); + } + if (session2 != null) { + session2.logout(); + } + } + } + + private static void check(Session session) throws RepositoryException { + if (session.getNode("/testNode").getProperty("p1").getLong() + + session.getNode("/testNode").getProperty("p2").getLong() < 0) { + fail("p1 + p2 < 0"); + } + } + + @Test + public void move() throws RepositoryException { + Session session = getAdminSession(); + + Node node = session.getNode("/"); + node.addNode("source").addNode("node"); + node.addNode("target"); + session.save(); + + session.refresh(true); + Node sourceNode = session.getNode("/source/node"); + session.move("/source/node", "/target/moved"); + // assertEquals("/target/moved", sourceNode.getPath()); // passes on JR2, fails on Oak + try { + sourceNode.getPath(); + } + catch (InvalidItemStateException expected) { + // sourceNode is stale + } + } + + @Test + public void move2() throws CommitFailedException { + ContentSession session = new Oak().createContentSession(); + Root root = session.getLatestRoot(); + root.getTree("/").addChild("x"); + root.getTree("/").addChild("y"); + root.commit(); + + Tree r = root.getTree("/"); + Tree x = r.getChild("x"); + Tree y = r.getChild("y"); + + assertFalse(y.hasChild("x")); + assertEquals("", x.getParent().getName()); + root.move("/x", "/y/x"); + assertTrue(y.hasChild("x")); + // assertEquals("y", x.getParent().getName()); // passed on JR2, fails on Oak + assertEquals("", x.getParent().getName()); // fails on JR2, passes on Oak + } + +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/OakRepositoryStub.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/OakRepositoryStub.java new file mode 100644 index 00000000000..f4376c1f4e8 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/OakRepositoryStub.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr; + +import java.security.Principal; +import java.util.Properties; +import java.util.concurrent.Executors; +import javax.jcr.Credentials; +import javax.jcr.GuestCredentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.security.auth.login.Configuration; + +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +import org.apache.jackrabbit.oak.security.OakConfiguration; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.RepositoryStub; + +public class OakRepositoryStub extends RepositoryStub { + + private final Repository repository; + + /** + * Constructor as required by the JCR TCK. + * + * @param settings repository settings + * @throws javax.jcr.RepositoryException If an error occurs. + */ + public OakRepositoryStub(Properties settings) throws RepositoryException { + super(settings); + + // TODO: OAK-17. workaround for missing test configuration + Configuration.setConfiguration(new OakConfiguration()); + + String dir = "target/mk-tck-" + System.currentTimeMillis(); + repository = new Jcr(new MicroKernelImpl(dir)) + .with(Executors.newScheduledThreadPool(1)) + .createRepository(); + + Session session = repository.login(superuser); + try { + TestContentLoader loader = new TestContentLoader(); + loader.loadTestContent(session); + } catch (Exception e) { + e.printStackTrace(System.err); + } finally { + session.logout(); + } + } + + /** + * Returns the configured repository instance. + * + * @return the configured repository instance. + */ + @Override + public synchronized Repository getRepository() { + return repository; + } + + @Override + public Credentials getReadOnlyCredentials() { + return new GuestCredentials(); + } + + @Override + public Principal getKnownPrincipal(Session session) throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + private static final Principal UNKNOWN_PRINCIPAL = new Principal() { + @Override + public String getName() { + return "an_unknown_user"; + } + }; + + @Override + public Principal getUnknownPrincipal(Session session) throws RepositoryException, NotExecutableException { + return UNKNOWN_PRINCIPAL; + } + +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/OrderableNodesTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/OrderableNodesTest.java new file mode 100644 index 00000000000..22c67061ab3 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/OrderableNodesTest.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.jcr; + +import org.junit.Test; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; + +public class OrderableNodesTest extends AbstractRepositoryTest { + + @Test + public void testSimpleOrdering() throws RepositoryException { + doTest("nt:unstructured"); + } + + @Test + public void orderableFolder() throws Exception { + // check ordering with node type without a residual properties definition + new TestContentLoader().loadTestContent(getAdminSession()); + doTest("test:orderableFolder"); + } + + private void doTest(String nodeType) throws RepositoryException { + Session session = getAdminSession(); + Node root = session.getRootNode().addNode("test", nodeType); + + root.addNode("a"); + root.addNode("b"); + root.addNode("c"); + + NodeIterator iterator; + + root.orderBefore("a", "b"); + root.orderBefore("c", null); + iterator = root.getNodes(); + assertEquals("a", iterator.nextNode().getName()); + assertEquals("b", iterator.nextNode().getName()); + assertEquals("c", iterator.nextNode().getName()); + assertFalse(iterator.hasNext()); + + root.orderBefore("c", "a"); + iterator = root.getNodes(); + assertEquals("c", iterator.nextNode().getName()); + assertEquals("a", iterator.nextNode().getName()); + assertEquals("b", iterator.nextNode().getName()); + assertFalse(iterator.hasNext()); + + root.orderBefore("b", "c"); + iterator = root.getNodes(); + assertEquals("b", iterator.nextNode().getName()); + assertEquals("c", iterator.nextNode().getName()); + assertEquals("a", iterator.nextNode().getName()); + assertFalse(iterator.hasNext()); + + session.save(); + } +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/RepositoryTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/RepositoryTest.java new file mode 100644 index 00000000000..063f1c265a4 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/RepositoryTest.java @@ -0,0 +1,1969 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; + +import javax.jcr.Binary; +import javax.jcr.GuestCredentials; +import javax.jcr.InvalidItemStateException; +import javax.jcr.Item; +import javax.jcr.NamespaceException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeTypeTemplate; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.EventListener; +import javax.jcr.observation.ObservationManager; + +import com.google.common.collect.Sets; +import org.apache.jackrabbit.JcrConstants; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class RepositoryTest extends AbstractRepositoryTest { + private static final String TEST_NODE = "test_node"; + private static final String TEST_PATH = '/' + TEST_NODE; + + @Before + public void setup() throws RepositoryException { + executor = Executors.newScheduledThreadPool(1); + + Session session = getAdminSession(); + ValueFactory valueFactory = session.getValueFactory(); + Node root = session.getRootNode(); + Node foo = root.addNode("foo"); + foo.setProperty("stringProp", "stringVal"); + foo.setProperty("intProp", 42); + foo.setProperty("mvProp", new Value[]{ + valueFactory.createValue(1), + valueFactory.createValue(2), + valueFactory.createValue(3), + }); + root.addNode("bar"); + root.addNode(TEST_NODE); + session.save(); + } + + @Test + public void createRepository() throws RepositoryException { + Repository repository = getRepository(); + assertNotNull(repository); + } + + @Test + public void login() throws RepositoryException { + assertNotNull(getAdminSession()); + } + + @Test(expected = NoSuchWorkspaceException.class) + public void loginInvalidWorkspace() throws RepositoryException { + Repository repository = getRepository(); + repository.login(new GuestCredentials(), "invalid"); + } + + @Ignore("OAK-118") // TODO OAK-118: implement workspace management + @Test + public void getWorkspaceNames() throws RepositoryException { + String[] workspaces = getAdminSession().getWorkspace().getAccessibleWorkspaceNames(); + + Set names = new HashSet() {{ + add("default"); + }}; + + assertTrue(asList(workspaces).containsAll(names)); + assertTrue(names.containsAll(asList(workspaces))); + } + + @Ignore("OAK-118") // TODO OAK-118: implement workspace management + @Test + public void createDeleteWorkspace() throws RepositoryException { + getAdminSession().getWorkspace().createWorkspace("new"); + + Session session2 = createAdminSession(); + try { + String[] workspaces = session2.getWorkspace().getAccessibleWorkspaceNames(); + assertTrue(asList(workspaces).contains("new")); + Session session3 = getRepository().login("new"); + assertEquals("new", session3.getWorkspace().getName()); + session3.logout(); + session2.getWorkspace().deleteWorkspace("new"); + } finally { + session2.logout(); + } + + Session session4 = createAdminSession(); + try { + String[] workspaces = session4.getWorkspace().getAccessibleWorkspaceNames(); + assertFalse(asList(workspaces).contains("new")); + } finally { + session4.logout(); + } + } + + @Test + public void getRoot() throws RepositoryException { + Node root = getAdminSession().getRootNode(); + assertNotNull(root); + assertEquals("", root.getName()); + assertEquals("/", root.getPath()); + } + + @Test + public void getRootFromPath() throws RepositoryException { + Node root = getNode("/"); + assertEquals("", root.getName()); + assertEquals("/", root.getPath()); + } + + @Test + public void getNode2() throws RepositoryException { + Node node = getNode("/foo"); + Node same = node.getNode("."); + assertNotNull(same); + assertEquals("foo", same.getName()); + assertTrue(same.isSame(node)); + } + + @Ignore("OAK-369") + @Test + public void getNode3() throws RepositoryException { + Node node = getNode("/foo"); + Node root = node.getNode(".."); + assertNotNull(root); + assertEquals("", root.getName()); + assertTrue("/".equals(root.getPath())); + } + + @Test + public void getNode() throws RepositoryException { + Node node = getNode("/foo"); + assertNotNull(node); + assertEquals("foo", node.getName()); + assertEquals("/foo", node.getPath()); + } + + @Test + public void getNodeByIdentifier() throws RepositoryException { + Node node = getNode("/foo"); + String id = node.getIdentifier(); + Node node2 = getAdminSession().getNodeByIdentifier(id); + assertTrue(node.isSame(node2)); + } + + @Ignore("OAK-343") + @Test + public void getNodeByUUID() throws RepositoryException { + Node node = getNode("/foo").addNode("boo"); + node.addMixin(JcrConstants.MIX_REFERENCEABLE); + + assertTrue(node.isNodeType(JcrConstants.MIX_REFERENCEABLE)); + String uuid = node.getUUID(); + assertNotNull(uuid); + assertEquals(uuid, node.getIdentifier()); + + Node nAgain = node.getSession().getNodeByUUID(uuid); + assertTrue(nAgain.isSame(node)); + assertTrue(nAgain.isSame(node.getSession().getNodeByIdentifier(uuid))); + } + + @Test + public void getNodeFromNode() throws RepositoryException { + Node root = getNode("/"); + Node node = root.getNode("foo"); + assertNotNull(node); + assertEquals("foo", node.getName()); + assertEquals("/foo", node.getPath()); + + Node nodeAgain = getNode("/foo"); + assertTrue(node.isSame(nodeAgain)); + } + + @Test + public void getNodes() throws RepositoryException { + Set nodeNames = new HashSet() {{ + add("bar"); + add("added"); + add(TEST_NODE); + }}; + + Node root = getNode("/"); + root.addNode("added"); // transiently added + root.getNode("foo").remove(); // transiently removed + root.getNode("bar").remove(); // transiently removed and... + root.addNode("bar"); // ... added again + NodeIterator nodes = root.getNodes(); + // FIXME: use a test subtree to avoid excluding default content + int expected = 3 + + (root.hasNode("jcr:system") ? 1 : 0) + + (root.hasNode("rep:security") ? 1 : 0) + + (root.hasNode("oak-index") ? 1 : 0) + + (root.hasNode("oak:index") ? 1 : 0); + assertEquals(expected, nodes.getSize()); + while (nodes.hasNext()) { + String name = nodes.nextNode().getName(); + if (!name.equals("jcr:system") + && !name.equals("rep:security") + && !name.equals("oak-index") + && !name.equals("oak:index")) { + assertTrue(nodeNames.remove(name)); + } + } + + assertTrue(nodeNames.isEmpty()); + } + + @Test + public void getProperties() throws RepositoryException { + Set propertyNames = new HashSet() {{ + add("intProp"); + add("mvProp"); + add("added"); + }}; + + Set values = new HashSet() {{ + add("added"); + add("1"); + add("2"); + add("3"); + add("42"); + }}; + + Node node = getNode("/foo"); + node.setProperty("added", "added"); // transiently added + node.getProperty("stringProp").remove(); // transiently removed + node.getProperty("intProp").remove(); // transiently removed... + node.setProperty("intProp", 42); // ...and added again + PropertyIterator properties = node.getProperties(); + assertEquals(4, properties.getSize()); + while (properties.hasNext()) { + Property p = properties.nextProperty(); + if (JcrConstants.JCR_PRIMARYTYPE.equals(p.getName())) { + continue; + } + assertTrue(propertyNames.remove(p.getName())); + if (p.isMultiple()) { + for (Value v : p.getValues()) { + assertTrue(values.remove(v.getString())); + } + } else { + assertTrue(values.remove(p.getString())); + } + } + + assertTrue(propertyNames.isEmpty()); + assertTrue(values.isEmpty()); + } + + @Test(expected = PathNotFoundException.class) + public void getNonExistingNode() throws RepositoryException { + getNode("/qoo"); + } + + @Test + public void getProperty() throws RepositoryException { + Property property = getProperty("/foo/stringProp"); + assertNotNull(property); + assertEquals("stringProp", property.getName()); + assertEquals("/foo/stringProp", property.getPath()); + + Value value = property.getValue(); + assertNotNull(value); + assertEquals(PropertyType.STRING, value.getType()); + assertEquals("stringVal", value.getString()); + } + + @Test + public void getPropertyFromNode() throws RepositoryException { + Node node = getNode("/foo"); + Property property = node.getProperty("stringProp"); + assertNotNull(property); + assertEquals("stringProp", property.getName()); + assertEquals("/foo/stringProp", property.getPath()); + + Value value = property.getValue(); + assertNotNull(value); + assertEquals(PropertyType.STRING, value.getType()); + assertEquals("stringVal", value.getString()); + + Property propertyAgain = getProperty("/foo/stringProp"); + assertTrue(property.isSame(propertyAgain)); + } + + @Test + public void addNode() throws RepositoryException { + Session session = getAdminSession(); + String newNode = TEST_PATH + "/new"; + assertFalse(session.nodeExists(newNode)); + + Node node = getNode(TEST_PATH); + Node added = node.addNode("new"); + assertFalse(node.isNew()); + assertTrue(node.isModified()); + assertTrue(added.isNew()); + assertFalse(added.isModified()); + session.save(); + + Session session2 = createAnonymousSession(); + try { + assertTrue(session2.nodeExists(newNode)); + added = session2.getNode(newNode); + assertFalse(added.isNew()); + assertFalse(added.isModified()); + } finally { + session2.logout(); + } + } + + @Test + public void addNodeWithSpecialChars() throws RepositoryException { + Session session = getAdminSession(); + String nodeName = "foo{"; + + String newNode = TEST_PATH + '/' + nodeName; + assertFalse(session.nodeExists(newNode)); + + Node node = getNode(TEST_PATH); + node.addNode(nodeName); + session.save(); + + Session session2 = createAnonymousSession(); + try { + assertTrue(session2.nodeExists(newNode)); + } finally { + session2.logout(); + } + } + + @Test + public void addNodeWithNodeType() throws RepositoryException { + Session session = getAdminSession(); + + Node node = getNode(TEST_PATH); + node.addNode("new", "nt:folder"); + session.save(); + } + + @Test + public void addNodeToRootNode() throws RepositoryException { + Session session = getAdminSession(); + Node root = session.getRootNode(); + String newNode = "newNodeBelowRoot"; + assertFalse(root.hasNode(newNode)); + root.addNode(newNode); + session.save(); + } + + @Test + public void addStringProperty() throws RepositoryException, IOException { + Node parentNode = getNode(TEST_PATH); + addProperty(parentNode, "string", getAdminSession().getValueFactory().createValue("string value")); + } + + @Test + public void addMultiValuedString() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + Value[] values = new Value[2]; + values[0] = getAdminSession().getValueFactory().createValue("one"); + values[1] = getAdminSession().getValueFactory().createValue("two"); + + parentNode.setProperty("multi string", values); + parentNode.getSession().save(); + + Session session2 = createAdminSession(); + try { + Property property = session2.getProperty(TEST_PATH + "/multi string"); + assertTrue(property.isMultiple()); + assertEquals(PropertyType.STRING, property.getType()); + Value[] values2 = property.getValues(); + assertEquals(values.length, values2.length); + assertEquals(values[0], values2[0]); + assertEquals(values[1], values2[1]); + } finally { + session2.logout(); + } + } + + @Test + public void addLongProperty() throws RepositoryException, IOException { + Node parentNode = getNode(TEST_PATH); + addProperty(parentNode, "long", getAdminSession().getValueFactory().createValue(42L)); + } + + @Test + public void addMultiValuedLong() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + Value[] values = new Value[2]; + values[0] = getAdminSession().getValueFactory().createValue(42L); + values[1] = getAdminSession().getValueFactory().createValue(84L); + + parentNode.setProperty("multi long", values); + parentNode.getSession().save(); + + Session session2 = createAnonymousSession(); + try { + Property property = session2.getProperty(TEST_PATH + "/multi long"); + assertTrue(property.isMultiple()); + assertEquals(PropertyType.LONG, property.getType()); + Value[] values2 = property.getValues(); + assertEquals(values.length, values2.length); + assertEquals(values[0], values2[0]); + assertEquals(values[1], values2[1]); + } finally { + session2.logout(); + } + } + + @Test + public void addDoubleProperty() throws RepositoryException, IOException { + Node parentNode = getNode(TEST_PATH); + addProperty(parentNode, "double", getAdminSession().getValueFactory().createValue(42.2D)); + } + + @Test + public void addMultiValuedDouble() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + Value[] values = new Value[2]; + values[0] = getAdminSession().getValueFactory().createValue(42.1d); + values[1] = getAdminSession().getValueFactory().createValue(99.0d); + + parentNode.setProperty("multi double", values); + parentNode.getSession().save(); + + Session session2 = createAnonymousSession(); + try { + Property property = session2.getProperty(TEST_PATH + "/multi double"); + assertTrue(property.isMultiple()); + assertEquals(PropertyType.DOUBLE, property.getType()); + Value[] values2 = property.getValues(); + assertEquals(values.length, values2.length); + assertEquals(values[0], values2[0]); + assertEquals(values[1], values2[1]); + } finally { + session2.logout(); + } + } + + @Test + public void addBooleanProperty() throws RepositoryException, IOException { + Node parentNode = getNode(TEST_PATH); + addProperty(parentNode, "boolean", getAdminSession().getValueFactory().createValue(true)); + } + + @Test + public void addMultiValuedBoolean() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + Value[] values = new Value[2]; + values[0] = getAdminSession().getValueFactory().createValue(true); + values[1] = getAdminSession().getValueFactory().createValue(false); + + parentNode.setProperty("multi boolean", values); + parentNode.getSession().save(); + + Session session2 = createAnonymousSession(); + try { + Property property = session2.getProperty(TEST_PATH + "/multi boolean"); + assertTrue(property.isMultiple()); + assertEquals(PropertyType.BOOLEAN, property.getType()); + Value[] values2 = property.getValues(); + assertEquals(values.length, values2.length); + assertEquals(values[0], values2[0]); + assertEquals(values[1], values2[1]); + } finally { + session2.logout(); + } + } + + @Test + public void addDecimalProperty() throws RepositoryException, IOException { + Node parentNode = getNode(TEST_PATH); + addProperty(parentNode, "decimal", getAdminSession().getValueFactory().createValue(BigDecimal.valueOf(21))); + } + + @Test + public void addMultiValuedDecimal() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + Value[] values = new Value[2]; + values[0] = getAdminSession().getValueFactory().createValue(BigDecimal.valueOf(42)); + values[1] = getAdminSession().getValueFactory().createValue(BigDecimal.valueOf(99)); + + parentNode.setProperty("multi decimal", values); + parentNode.getSession().save(); + + Session session2 = createAnonymousSession(); + try { + Property property = session2.getProperty(TEST_PATH + "/multi decimal"); + assertTrue(property.isMultiple()); + assertEquals(PropertyType.DECIMAL, property.getType()); + Value[] values2 = property.getValues(); + assertEquals(values.length, values2.length); + assertEquals(values[0], values2[0]); + assertEquals(values[1], values2[1]); + } finally { + session2.logout(); + } + } + + @Test + public void addDateProperty() throws RepositoryException, IOException { + Node parentNode = getNode(TEST_PATH); + addProperty(parentNode, "date", getAdminSession().getValueFactory().createValue(Calendar.getInstance())); + } + + @Test + public void addMultiValuedDate() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + Value[] values = new Value[2]; + Calendar calendar = Calendar.getInstance(); + values[0] = getAdminSession().getValueFactory().createValue(calendar); + calendar.add(Calendar.DAY_OF_MONTH, 1); + values[1] = getAdminSession().getValueFactory().createValue(calendar); + + parentNode.setProperty("multi date", values); + parentNode.getSession().save(); + + Session session2 = createAnonymousSession(); + try { + Property property = session2.getProperty(TEST_PATH + "/multi date"); + assertTrue(property.isMultiple()); + assertEquals(PropertyType.DATE, property.getType()); + Value[] values2 = property.getValues(); + assertEquals(values.length, values2.length); + assertEquals(values[0], values2[0]); + assertEquals(values[1], values2[1]); + } finally { + session2.logout(); + } + } + + @Test + public void addURIProperty() throws RepositoryException, IOException { + Node parentNode = getNode(TEST_PATH); + addProperty(parentNode, "uri", getAdminSession().getValueFactory().createValue("http://www.day.com/", PropertyType.URI)); + } + + @Test + public void addMultiValuedURI() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + Value[] values = new Value[2]; + values[0] = getAdminSession().getValueFactory().createValue("http://www.day.com", PropertyType.URI); + values[1] = getAdminSession().getValueFactory().createValue("file://var/dam", PropertyType.URI); + + parentNode.setProperty("multi uri", values); + parentNode.getSession().save(); + + Session session2 = createAnonymousSession(); + try { + Property property = session2.getProperty(TEST_PATH + "/multi uri"); + assertTrue(property.isMultiple()); + assertEquals(PropertyType.URI, property.getType()); + Value[] values2 = property.getValues(); + assertEquals(values.length, values2.length); + assertEquals(values[0], values2[0]); + assertEquals(values[1], values2[1]); + } finally { + session2.logout(); + } + } + + @Test + public void addNameProperty() throws RepositoryException, IOException { + Node parentNode = getNode(TEST_PATH); + addProperty(parentNode, "name", getAdminSession().getValueFactory().createValue("jcr:something\"", PropertyType.NAME)); + } + + @Test + public void addMultiValuedName() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + Value[] values = new Value[2]; + values[0] = getAdminSession().getValueFactory().createValue("jcr:foo", PropertyType.NAME); + values[1] = getAdminSession().getValueFactory().createValue("bar", PropertyType.NAME); + + parentNode.setProperty("multi name", values); + parentNode.getSession().save(); + + Session session2 = createAnonymousSession(); + try { + Property property = session2.getProperty(TEST_PATH + "/multi name"); + assertTrue(property.isMultiple()); + assertEquals(PropertyType.NAME, property.getType()); + Value[] values2 = property.getValues(); + assertEquals(values.length, values2.length); + assertEquals(values[0], values2[0]); + assertEquals(values[1], values2[1]); + } finally { + session2.logout(); + } + } + + @Test + public void addPathProperty() throws RepositoryException, IOException { + Node parentNode = getNode(TEST_PATH); + addProperty(parentNode, "path", getAdminSession().getValueFactory().createValue("/jcr:foo/bar\"", PropertyType.PATH)); + } + + @Test + public void addMultiValuedPath() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + Value[] values = new Value[2]; + values[0] = getAdminSession().getValueFactory().createValue("/nt:foo/jcr:bar", PropertyType.PATH); + values[1] = getAdminSession().getValueFactory().createValue("/", PropertyType.PATH); + + parentNode.setProperty("multi path", values); + parentNode.getSession().save(); + + Session session2 = createAnonymousSession(); + try { + Property property = session2.getProperty(TEST_PATH + "/multi path"); + assertTrue(property.isMultiple()); + assertEquals(PropertyType.PATH, property.getType()); + Value[] values2 = property.getValues(); + assertEquals(values.length, values2.length); + assertEquals(values[0], values2[0]); + assertEquals(values[1], values2[1]); + } finally { + session2.logout(); + } + } + + @Test + public void addBinaryProperty() throws RepositoryException, IOException { + Node parentNode = getNode(TEST_PATH); + InputStream is = new ByteArrayInputStream("foo\"".getBytes()); + Binary bin = getAdminSession().getValueFactory().createBinary(is); + addProperty(parentNode, "binary", getAdminSession().getValueFactory().createValue(bin)); + } + + @Test + public void addSmallBinaryProperty() throws RepositoryException, IOException { + Node parentNode = getNode(TEST_PATH); + InputStream is = new NumberStream(1234); + Binary bin = getAdminSession().getValueFactory().createBinary(is); + addProperty(parentNode, "bigBinary", getAdminSession().getValueFactory().createValue(bin)); + } + + @Test + public void addBigBinaryProperty() throws RepositoryException, IOException { + Node parentNode = getNode(TEST_PATH); + InputStream is = new NumberStream(123456); + Binary bin = getAdminSession().getValueFactory().createBinary(is); + addProperty(parentNode, "bigBinary", getAdminSession().getValueFactory().createValue(bin)); + } + + @Test + public void addMultiValuedBinary() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + Value[] values = new Value[2]; + + InputStream is = new ByteArrayInputStream("foo".getBytes()); + Binary bin = getAdminSession().getValueFactory().createBinary(is); + values[0] = getAdminSession().getValueFactory().createValue(bin); + + is = new ByteArrayInputStream("bar".getBytes()); + bin = getAdminSession().getValueFactory().createBinary(is); + values[1] = getAdminSession().getValueFactory().createValue(bin); + + parentNode.setProperty("multi binary", values); + parentNode.getSession().save(); + + Session session2 = createAnonymousSession(); + try { + Property property = session2.getProperty(TEST_PATH + "/multi binary"); + assertTrue(property.isMultiple()); + assertEquals(PropertyType.BINARY, property.getType()); + Value[] values2 = property.getValues(); + assertEquals(values.length, values2.length); + assertEquals(values[0], values2[0]); + assertEquals(values[1], values2[1]); + } finally { + session2.logout(); + } + } + + @Test + public void addReferenceProperty() throws RepositoryException, IOException { + Node parentNode = getNode(TEST_PATH); + Node referee = getAdminSession().getNode("/foo"); + referee.addMixin("mix:referenceable"); + getAdminSession().save(); + + addProperty(parentNode, "reference", getAdminSession().getValueFactory().createValue(referee)); + } + + @Test + public void addMultiValuedReference() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + Node referee = getAdminSession().getNode("/foo"); + referee.addMixin("mix:referenceable"); + getAdminSession().save(); + + Value[] values = new Value[2]; + values[0] = getAdminSession().getValueFactory().createValue(referee); + values[1] = getAdminSession().getValueFactory().createValue(referee); + + parentNode.setProperty("multi reference", values); + parentNode.getSession().save(); + + Session session2 = createAnonymousSession(); + try { + Property property = session2.getProperty(TEST_PATH + "/multi reference"); + assertTrue(property.isMultiple()); + assertEquals(PropertyType.REFERENCE, property.getType()); + Value[] values2 = property.getValues(); + assertEquals(values.length, values2.length); + assertEquals(values[0], values2[0]); + assertEquals(values[1], values2[1]); + } finally { + session2.logout(); + } + } + + @Test + public void addWeakReferenceProperty() throws RepositoryException, IOException { + Node parentNode = getNode(TEST_PATH); + Node referee = getAdminSession().getNode("/foo"); + referee.addMixin("mix:referenceable"); + getAdminSession().save(); + + addProperty(parentNode, "weak reference", getAdminSession().getValueFactory().createValue(referee, true)); + } + + @Test + public void addMultiValuedWeakReference() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + Node referee = getAdminSession().getNode("/foo"); + referee.addMixin("mix:referenceable"); + getAdminSession().save(); + + Value[] values = new Value[2]; + values[0] = getAdminSession().getValueFactory().createValue(referee, true); + values[1] = getAdminSession().getValueFactory().createValue(referee, true); + + parentNode.setProperty("multi weak reference", values); + parentNode.getSession().save(); + + Session session2 = createAnonymousSession(); + try { + Property property = session2.getProperty(TEST_PATH + "/multi weak reference"); + assertTrue(property.isMultiple()); + assertEquals(PropertyType.WEAKREFERENCE, property.getType()); + Value[] values2 = property.getValues(); + assertEquals(values.length, values2.length); + assertEquals(values[0], values2[0]); + assertEquals(values[1], values2[1]); + } finally { + session2.logout(); + } + } + + @Test + public void addEmptyMultiValuedProperty() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + Value[] values = new Value[0]; + + parentNode.setProperty("multi empty", values, PropertyType.BOOLEAN); + parentNode.getSession().save(); + + Session session2 = createAnonymousSession(); + try { + Property property = session2.getProperty(TEST_PATH + "/multi empty"); + assertTrue(property.isMultiple()); + Value[] values2 = property.getValues(); + assertEquals(0, values2.length); + } finally { + session2.logout(); + } + } + + @Test + public void testEmptyMultiValuedPropertyType() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + Value[] values = new Value[0]; + + parentNode.setProperty("multi empty", values, PropertyType.BOOLEAN); + parentNode.getSession().save(); + + Session session2 = createAnonymousSession(); + try { + Property property = session2.getProperty(TEST_PATH + "/multi empty"); + assertTrue(property.isMultiple()); + assertEquals(PropertyType.STRING, property.getType()); + Value[] values2 = property.getValues(); + assertEquals(0, values2.length); + } finally { + session2.logout(); + } + } + + @Test + public void addMultiValuedStringWithNull() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + Value[] values = new Value[3]; + values[0] = getAdminSession().getValueFactory().createValue(true); + values[2] = getAdminSession().getValueFactory().createValue(false); + + parentNode.setProperty("multi with null", values); + parentNode.getSession().save(); + + Session session2 = createAnonymousSession(); + try { + Property property = session2.getProperty(TEST_PATH + "/multi with null"); + assertTrue(property.isMultiple()); + assertEquals(PropertyType.BOOLEAN, property.getType()); + Value[] values2 = property.getValues(); + assertEquals(values.length - 1, values2.length); + } finally { + session2.logout(); + } + } + + @Test + public void transientChanges() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + + Node node = parentNode.addNode("test"); + + assertFalse(node.hasProperty("p")); + node.setProperty("p", "pv"); + assertTrue(node.hasProperty("p")); + + assertFalse(node.hasNode("n")); + node.addNode("n"); + assertTrue(node.hasNode("n")); + + assertTrue(node.hasProperties()); + assertTrue(node.hasNodes()); + } + + @Test + public void setStringProperty() throws RepositoryException, IOException { + Node parentNode = getNode(TEST_PATH); + addProperty(parentNode, "string", getAdminSession().getValueFactory().createValue("string \" value")); + + Property property = parentNode.getProperty("string"); + property.setValue("new value"); + assertTrue(parentNode.isModified()); + assertTrue(property.isModified()); + assertFalse(property.isNew()); + property.getSession().save(); + + Session session2 = createAnonymousSession(); + try { + Property property2 = session2.getProperty(TEST_PATH + "/string"); + assertEquals("new value", property2.getString()); + } finally { + session2.logout(); + } + } + + @Test + public void setDoubleNaNProperty() throws RepositoryException, IOException { + Node parentNode = getNode(TEST_PATH); + addProperty(parentNode, "NaN", getAdminSession().getValueFactory().createValue(Double.NaN)); + + Session session2 = createAnonymousSession(); + try { + Property property2 = session2.getProperty(TEST_PATH + "/NaN"); + assertTrue(Double.isNaN(property2.getDouble())); + } finally { + session2.logout(); + } + } + + @Test + public void setMultiValuedProperty() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + Value[] values = new Value[2]; + values[0] = getAdminSession().getValueFactory().createValue("one"); + values[1] = getAdminSession().getValueFactory().createValue("two"); + + parentNode.setProperty("multi string2", values); + parentNode.getSession().save(); + + values[0] = getAdminSession().getValueFactory().createValue("eins"); + values[1] = getAdminSession().getValueFactory().createValue("zwei"); + parentNode.setProperty("multi string2", values); + parentNode.getSession().save(); + + Session session2 = createAnonymousSession(); + try { + Property property = session2.getProperty(TEST_PATH + "/multi string2"); + assertTrue(property.isMultiple()); + Value[] values2 = property.getValues(); + assertEquals(2, values.length); + assertEquals(values[0], values2[0]); + assertEquals(values[1], values2[1]); + } finally { + session2.logout(); + } + } + + @Test + public void nullProperty() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + parentNode.setProperty("newProperty", "some value"); + parentNode.getSession().save(); + + Session session2 = createAdminSession(); + try { + session2.getProperty(TEST_PATH + "/newProperty").setValue((String) null); + session2.save(); + } finally { + session2.logout(); + } + + Session session3 = createAnonymousSession(); + try { + assertFalse(session3.propertyExists(TEST_PATH + "/newProperty")); + } finally { + session3.logout(); + } + } + + @Test + public void removeProperty() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + parentNode.setProperty("newProperty", "some value"); + parentNode.getSession().save(); + + Session session2 = createAdminSession(); + try { + session2.getProperty(TEST_PATH + "/newProperty").remove(); + session2.save(); + } finally { + session2.logout(); + } + + Session session3 = createAnonymousSession(); + try { + assertFalse(session3.propertyExists(TEST_PATH + "/newProperty")); + } finally { + session3.logout(); + } + } + + @Test + public void removeNode() throws RepositoryException { + Node parentNode = getNode(TEST_PATH); + parentNode.addNode("newNode"); + parentNode.getSession().save(); + + Session session2 = createAdminSession(); + try { + Node removeNode = session2.getNode(TEST_PATH + "/newNode"); + removeNode.remove(); + + try { + removeNode.getParent(); + fail("Cannot retrieve the parent from a transiently removed item."); + } catch (InvalidItemStateException expected) { + } + + assertTrue(session2.getNode(TEST_PATH).isModified()); + session2.save(); + } finally { + session2.logout(); + } + + Session session3 = createAnonymousSession(); + try { + assertFalse(session3.nodeExists(TEST_PATH + "/newNode")); + assertFalse(session3.getNode(TEST_PATH).isModified()); + } finally { + session3.logout(); + } + } + + @Test + public void accessRemovedItem() throws RepositoryException { + Node foo = getNode("/foo"); + Node bar = foo.addNode("bar"); + Property p = bar.setProperty("name", "value"); + foo.remove(); + try { + bar.getPath(); + fail("Expected InvalidItemStateException"); + } + catch (InvalidItemStateException expected) { + } + try { + p.getPath(); + fail("Expected InvalidItemStateException"); + } + catch (InvalidItemStateException expected) { + } + } + + @Test + public void accessRemovedProperty() throws RepositoryException { + Node foo = getNode("/foo"); + Property p = foo.setProperty("name", "value"); + p.remove(); + try { + p.getPath(); + fail("Expected InvalidItemStateException"); + } + catch (InvalidItemStateException expected) { + } + } + + @Test + public void getReferences() throws RepositoryException { + Session session = getAdminSession(); + Node referee = getNode("/foo"); + referee.addMixin("mix:referenceable"); + getNode(TEST_PATH).setProperty("reference", session.getValueFactory().createValue(referee)); + session.save(); + + PropertyIterator refs = referee.getReferences(); + assertTrue(refs.hasNext()); + Property p = refs.nextProperty(); + assertEquals("reference", p.getName()); + assertFalse(refs.hasNext()); + } + + @Test + public void getNamedReferences() throws RepositoryException { + Session session = getAdminSession(); + Node referee = getNode("/foo"); + referee.addMixin("mix:referenceable"); + Value value = session.getValueFactory().createValue(referee); + getNode(TEST_PATH).setProperty("reference1", value); + getNode("/bar").setProperty("reference2", value); + session.save(); + + PropertyIterator refs = referee.getReferences("reference1"); + assertTrue(refs.hasNext()); + Property p = refs.nextProperty(); + assertEquals("reference1", p.getName()); + assertFalse(refs.hasNext()); + } + + @Test + public void getWeakReferences() throws RepositoryException { + Session session = getAdminSession(); + Node referee = getNode("/foo"); + referee.addMixin("mix:referenceable"); + getNode(TEST_PATH).setProperty("weak-reference", session.getValueFactory().createValue(referee, true)); + session.save(); + + PropertyIterator refs = referee.getWeakReferences(); + assertTrue(refs.hasNext()); + Property p = refs.nextProperty(); + assertEquals("weak-reference", p.getName()); + assertFalse(refs.hasNext()); + } + + @Test + public void getNamedWeakReferences() throws RepositoryException { + Session session = getAdminSession(); + Node referee = getNode("/foo"); + referee.addMixin("mix:referenceable"); + Value value = session.getValueFactory().createValue(referee, true); + getNode(TEST_PATH).setProperty("weak-reference1", value); + getNode("/bar").setProperty("weak-reference2", value); + session.save(); + + PropertyIterator refs = referee.getWeakReferences("weak-reference1"); + assertTrue(refs.hasNext()); + Property p = refs.nextProperty(); + assertEquals("weak-reference1", p.getName()); + assertFalse(refs.hasNext()); + } + + @Test + public void sessionSave() throws RepositoryException { + Session session1 = createAdminSession(); + Session session2 = createAdminSession(); + try { + // Add some items and ensure they are accessible through this session + session1.getNode("/").addNode("node1"); + session1.getNode("/node1").addNode("node2"); + session1.getNode("/").addNode("node1/node3"); + + Node node1 = session1.getNode("/node1"); + assertEquals("/node1", node1.getPath()); + + Node node2 = session1.getNode("/node1/node2"); + assertEquals("/node1/node2", node2.getPath()); + + Node node3 = session1.getNode("/node1/node3"); + assertEquals("/node1/node3", node3.getPath()); + + node3.setProperty("property1", "value1"); + Item property1 = session1.getProperty("/node1/node3/property1"); + assertFalse(property1.isNode()); + assertEquals("value1", ((Property) property1).getString()); + + // Make sure these items are not accessible through another session + assertFalse(session2.itemExists("/node1")); + assertFalse(session2.itemExists("/node1/node2")); + assertFalse(session2.itemExists("/node1/node3")); + assertFalse(session2.itemExists("/node1/node3/property1")); + + session1.save(); + + // Make sure these items are still not accessible through another session until refresh + assertFalse(session2.itemExists("/node1")); + assertFalse(session2.itemExists("/node1/node2")); + assertFalse(session2.itemExists("/node1/node3")); + assertFalse(session2.itemExists("/node1/node3/property1")); + + session2.refresh(false); + assertTrue(session2.itemExists("/node1")); + assertTrue(session2.itemExists("/node1/node2")); + assertTrue(session2.itemExists("/node1/node3")); + assertTrue(session2.itemExists("/node1/node3/property1")); + } finally { + session1.logout(); + session2.logout(); + } + } + + @Test + public void sessionRefresh() throws RepositoryException { + Session session = getAdminSession(); + // Add some items and ensure they are accessible through this session + session.getNode("/").addNode("node1"); + session.getNode("/node1").addNode("node2"); + session.getNode("/").addNode("node1/node3"); + + Node node1 = session.getNode("/node1"); + assertEquals("/node1", node1.getPath()); + + Node node2 = session.getNode("/node1/node2"); + assertEquals("/node1/node2", node2.getPath()); + + Node node3 = session.getNode("/node1/node3"); + assertEquals("/node1/node3", node3.getPath()); + + node3.setProperty("property1", "value1"); + Item property1 = session.getProperty("/node1/node3/property1"); + assertFalse(property1.isNode()); + assertEquals("value1", ((Property) property1).getString()); + + // Make sure these items are still accessible after refresh(true); + session.refresh(true); + assertTrue(session.itemExists("/node1")); + assertTrue(session.itemExists("/node1/node2")); + assertTrue(session.itemExists("/node1/node3")); + assertTrue(session.itemExists("/node1/node3/property1")); + + session.refresh(false); + // Make sure these items are not accessible after refresh(false); + assertFalse(session.itemExists("/node1")); + assertFalse(session.itemExists("/node1/node2")); + assertFalse(session.itemExists("/node1/node3")); + assertFalse(session.itemExists("/node1/node3/property1")); + } + + @Test + public void sessionRefreshFalse() throws RepositoryException { + Session session1 = createAdminSession(); + Session session2 = createAdminSession(); + try { + Node foo = session1.getNode("/foo"); + foo.addNode("added"); + + session2.getNode("/foo").addNode("bar"); + session2.save(); + + session1.refresh(false); + assertFalse(foo.hasNode("added")); + assertTrue(foo.hasNode("bar")); + } finally { + session1.logout(); + session2.logout(); + } + } + + @Test + public void sessionRefreshTrue() throws RepositoryException { + Session session1 = createAdminSession(); + Session session2 = createAdminSession(); + try { + Node foo = session1.getNode("/foo"); + foo.addNode("added"); + + session2.getNode("/foo").addNode("bar"); + session2.save(); + + session1.refresh(true); + assertTrue(foo.hasNode("added")); + assertTrue(foo.hasNode("bar")); + } finally { + session1.logout(); + session2.logout(); + } + } + + @Test + public void sessionIsolation() throws RepositoryException { + Session session1 = createAdminSession(); + Session session2 = createAdminSession(); + try { + session1.getRootNode().addNode("node1"); + session2.getRootNode().addNode("node2"); + assertTrue(session1.getRootNode().hasNode("node1")); + assertTrue(session2.getRootNode().hasNode("node2")); + assertFalse(session1.getRootNode().hasNode("node2")); + assertFalse(session2.getRootNode().hasNode("node1")); + + session1.save(); + session2.save(); + assertTrue(session1.getRootNode().hasNode("node1")); + assertFalse(session1.getRootNode().hasNode("node2")); + assertTrue(session2.getRootNode().hasNode("node1")); + assertTrue(session2.getRootNode().hasNode("node2")); + } finally { + session1.logout(); + session2.logout(); + } + } + + @Test + public void saveRefreshConflict() throws RepositoryException { + Session session1 = createAdminSession(); + Session session2 = createAdminSession(); + try { + session1.getRootNode().addNode("node"); + session2.getRootNode().addNode("node").setProperty("p", "v"); + assertTrue(session1.getRootNode().hasNode("node")); + assertTrue(session2.getRootNode().hasNode("node")); + + session1.save(); + assertTrue(session1.getRootNode().hasNode("node")); + assertTrue(session2.getRootNode().hasNode("node")); + + session2.refresh(true); + assertTrue(session1.getRootNode().hasNode("node")); + assertTrue(session2.getRootNode().hasNode("node")); + + try { + session2.save(); + fail("Expected InvalidItemStateException"); + } catch (InvalidItemStateException expected) { + } + } finally { + session1.logout(); + session2.logout(); + } + } + + @Test + public void saveConflict() throws RepositoryException { + getAdminSession().getRootNode().addNode("node"); + getAdminSession().save(); + + Session session1 = createAdminSession(); + Session session2 = createAdminSession(); + try { + session1.getNode("/node").remove(); + session2.getNode("/node").addNode("2"); + assertFalse(session1.getRootNode().hasNode("node")); + assertTrue(session2.getRootNode().hasNode("node")); + assertTrue(session2.getRootNode().getNode("node").hasNode("2")); + + session1.save(); + assertFalse(session1.getRootNode().hasNode("node")); + assertTrue(session2.getRootNode().hasNode("node")); + assertTrue(session2.getRootNode().getNode("node").hasNode("2")); + + try { + session2.save(); + fail("Expected InvalidItemStateException"); + } catch (InvalidItemStateException expected) { + } + } finally { + session1.logout(); + session2.logout(); + } + } + + @Test + public void liveNodes() throws RepositoryException { + Session session = getAdminSession(); + Node n1 = (Node) session.getItem(TEST_PATH); + Node n2 = (Node) session.getItem(TEST_PATH); + + Node c1 = n1.addNode("c1"); + Node c2 = n2.addNode("c2"); + assertTrue(n1.hasNode("c1")); + assertTrue(n1.hasNode("c2")); + assertTrue(n2.hasNode("c1")); + assertTrue(n2.hasNode("c2")); + + c1.remove(); + assertFalse(n1.hasNode("c1")); + assertTrue(n1.hasNode("c2")); + assertFalse(n2.hasNode("c1")); + assertTrue(n2.hasNode("c2")); + + c2.remove(); + assertFalse(n1.hasNode("c1")); + assertFalse(n1.hasNode("c2")); + assertFalse(n2.hasNode("c1")); + assertFalse(n2.hasNode("c2")); + } + + @Test + public void move() throws RepositoryException { + Session session = getAdminSession(); + + Node node = getNode(TEST_PATH); + node.addNode("source").addNode("node"); + node.addNode("target"); + session.save(); + + session.refresh(true); + session.move(TEST_PATH + "/source/node", TEST_PATH + "/target/moved"); + + assertFalse(node.hasNode("source/node")); + assertTrue(node.hasNode("source")); + assertTrue(node.hasNode("target/moved")); + + session.save(); + + assertFalse(node.hasNode("source/node")); + assertTrue(node.hasNode("source")); + assertTrue(node.hasNode("target/moved")); + } + + @Test + public void moveReferenceable() throws RepositoryException { + Session session = getAdminSession(); + + Node node = getNode(TEST_PATH); + node.addNode("source").addNode("node").addMixin("mix:referenceable"); + node.addNode("target"); + session.save(); + + session.refresh(true); + session.move(TEST_PATH + "/source/node", TEST_PATH + "/target/moved"); + + assertFalse(node.hasNode("source/node")); + assertTrue(node.hasNode("source")); + assertTrue(node.hasNode("target/moved")); + + session.save(); + + assertFalse(node.hasNode("source/node")); + assertTrue(node.hasNode("source")); + assertTrue(node.hasNode("target/moved")); + } + + @Test + public void workspaceMove() throws RepositoryException { + Session session = getAdminSession(); + + Node node = getNode(TEST_PATH); + node.addNode("source").addNode("node"); + node.addNode("target"); + session.save(); + + session.getWorkspace().move(TEST_PATH + "/source/node", TEST_PATH + "/target/moved"); + + // Move must not be visible in session + assertTrue(node.hasNode("source/node")); + assertFalse(node.hasNode("target/moved")); + + session.refresh(false); + + // Move must be visible in session after refresh + assertFalse(node.hasNode("source/node")); + assertTrue(node.hasNode("source")); + assertTrue(node.hasNode("target/moved")); + } + + @Test + public void workspaceCopy() throws RepositoryException { + Session session = getAdminSession(); + + Node node = getNode(TEST_PATH); + node.addNode("source").addNode("node"); + node.addNode("target"); + session.save(); + + session.getWorkspace().copy(TEST_PATH + "/source/node", TEST_PATH + "/target/copied"); + + // Copy must not be visible in session + assertTrue(node.hasNode("source/node")); + assertFalse(node.hasNode("target/copied")); + + session.refresh(false); + + // Copy must be visible in session after refresh + assertTrue(node.hasNode("source/node")); + assertTrue(node.hasNode("target/copied")); + } + + @Test + public void setPrimaryType() throws RepositoryException { + Node testNode = getNode(TEST_PATH); + assertEquals("nt:unstructured", testNode.getPrimaryNodeType().getName()); + assertEquals("nt:unstructured", testNode.getProperty("jcr:primaryType").getString()); + + testNode.setPrimaryType("nt:folder"); + getAdminSession().save(); + + Session session2 = createAnonymousSession(); + try { + testNode = session2.getNode(TEST_PATH); + assertEquals("nt:folder", testNode.getPrimaryNodeType().getName()); + assertEquals("nt:folder", testNode.getProperty("jcr:primaryType").getString()); + } finally { + session2.logout(); + } + } + + @Test + public void nodeTypeRegistry() throws RepositoryException { + NodeTypeManager ntMgr = getAdminSession().getWorkspace().getNodeTypeManager(); + assertFalse(ntMgr.hasNodeType("foo")); + + NodeTypeTemplate ntd = ntMgr.createNodeTypeTemplate(); + ntd.setName("foo"); + ntMgr.registerNodeType(ntd, false); + assertTrue(ntMgr.hasNodeType("foo")); + + ntMgr.unregisterNodeType("foo"); + assertFalse(ntMgr.hasNodeType("foo")); + } + + @Test + public void testNamespaceRegistry() throws RepositoryException { + NamespaceRegistry nsReg = + getAdminSession().getWorkspace().getNamespaceRegistry(); + assertFalse(asList(nsReg.getPrefixes()).contains("foo")); + assertFalse(asList(nsReg.getURIs()).contains("file:///foo")); + + nsReg.registerNamespace("foo", "file:///foo"); + assertTrue(asList(nsReg.getPrefixes()).contains("foo")); + assertTrue(asList(nsReg.getURIs()).contains("file:///foo")); + + nsReg.unregisterNamespace("foo"); + assertFalse(asList(nsReg.getPrefixes()).contains("foo")); + assertFalse(asList(nsReg.getURIs()).contains("file:///foo")); + } + + @Test + public void sessionRemappedNamespace() throws RepositoryException { + NamespaceRegistry nsReg = + getAdminSession().getWorkspace().getNamespaceRegistry(); + nsReg.registerNamespace("foo", "file:///foo"); + + getAdminSession().getRootNode().addNode("foo:test"); + getAdminSession().save(); + + Session s = createAdminSession(); + s.setNamespacePrefix("bar", "file:///foo"); + assertTrue(s.getRootNode().hasNode("bar:test")); + Node n = s.getRootNode().getNode("bar:test"); + assertEquals("bar:test", n.getName()); + s.logout(); + + getAdminSession().getRootNode().getNode("foo:test").remove(); + getAdminSession().save(); + nsReg.unregisterNamespace("foo"); + } + + @Test + public void registryRemappedNamespace() throws RepositoryException { + NamespaceRegistry nsReg = + getAdminSession().getWorkspace().getNamespaceRegistry(); + nsReg.registerNamespace("foo", "file:///foo"); + + getAdminSession().getRootNode().addNode("foo:test"); + getAdminSession().save(); + + try { + nsReg.registerNamespace("bar", "file:///foo"); + fail("Remapping namespace through NamespaceRegistry must not be allowed"); + } catch (NamespaceException e) { + // expected + } finally { + getAdminSession().getRootNode().getNode("foo:test").remove(); + getAdminSession().save(); + + nsReg.unregisterNamespace("foo"); + } + } + + @Test + public void mixin() throws RepositoryException { + NodeTypeManager ntMgr = getAdminSession().getWorkspace().getNodeTypeManager(); + NodeTypeTemplate mixTest = ntMgr.createNodeTypeTemplate(); + mixTest.setName("mix:test"); + mixTest.setMixin(true); + ntMgr.registerNodeType(mixTest, false); + + Node testNode = getNode(TEST_PATH); + NodeType[] mix = testNode.getMixinNodeTypes(); + assertEquals(0, mix.length); + + testNode.addMixin("mix:test"); + testNode.getSession().save(); + + Session session2 = createAnonymousSession(); + try { + mix = session2.getNode(TEST_PATH).getMixinNodeTypes(); + assertEquals(1, mix.length); + assertEquals("mix:test", mix[0].getName()); + } finally { + session2.logout(); + } + + try { + testNode.removeMixin("mix:test"); + fail("Expected ConstraintViolationException"); + } catch (ConstraintViolationException expected) { + } + } + + @Test + public void observation() throws RepositoryException, InterruptedException { + final Set addNodes = Sets.newHashSet( + TEST_PATH + "/1", + TEST_PATH + "/2", + TEST_PATH + "/3"); + + final Set removeNodes = Sets.newHashSet( + TEST_PATH + "/2"); + + final Set addProperties = Sets.newHashSet( + TEST_PATH + "/property", + TEST_PATH + "/prop0", + TEST_PATH + "/1/prop1", + TEST_PATH + "/1/prop2", + TEST_PATH + "/1/jcr:primaryType", + TEST_PATH + "/2/jcr:primaryType", + TEST_PATH + "/3/jcr:primaryType", + TEST_PATH + "/3/prop3"); + + final Set setProperties = Sets.newHashSet( + TEST_PATH + "/1/prop1"); + + final Set removeProperties = Sets.newHashSet( + TEST_PATH + "/1/prop2", + TEST_PATH + "/2/jcr:primaryType"); + + final List failedEvents = new ArrayList(); + final AtomicReference eventCount = new AtomicReference(); + final Session observingSession = createAnonymousSession(); + try { + ObservationManager obsMgr = observingSession.getWorkspace().getObservationManager(); + obsMgr.addEventListener(new EventListener() { + @Override + public void onEvent(EventIterator events) { + while (events.hasNext()) { + Event event = events.nextEvent(); + try { + String path = event.getPath(); + if (path.startsWith("/jcr:system")) { + // ignore changes in jcr:system + continue; + } + switch (event.getType()) { + case Event.NODE_ADDED: + if (!addNodes.remove(path)) { + failedEvents.add(event); + } + if (!observingSession.nodeExists(path)) { + failedEvents.add(event); + } + break; + case Event.NODE_REMOVED: + if (!removeNodes.remove(path)) { + failedEvents.add(event); + } + if (observingSession.nodeExists(path)) { + failedEvents.add(event); + } + break; + case Event.PROPERTY_ADDED: + if (!addProperties.remove(path)) { + failedEvents.add(event); + } + if (!observingSession.propertyExists(path)) { + failedEvents.add(event); + } + break; + case Event.PROPERTY_CHANGED: + if (!setProperties.remove(path)) { + failedEvents.add(event); + } + break; + case Event.PROPERTY_REMOVED: + if (!removeProperties.remove(path)) { + failedEvents.add(event); + } + if (observingSession.propertyExists(path)) { + failedEvents.add(event); + } + break; + default: + failedEvents.add(event); + } + } catch (RepositoryException e) { + failedEvents.add(event); + } + eventCount.get().countDown(); + } + } + }, + Event.NODE_ADDED | Event.NODE_REMOVED | Event.NODE_MOVED | Event.PROPERTY_ADDED | + Event.PROPERTY_REMOVED | Event.PROPERTY_CHANGED | Event.PERSIST, "/", true, null, null, false); + + eventCount.set(new CountDownLatch(7)); + Node n = getNode(TEST_PATH); + n.setProperty("prop0", "val0"); + Node n1 = n.addNode("1"); + n1.setProperty("prop1", "val1"); + n1.setProperty("prop2", "val2"); + n.addNode("2"); + getAdminSession().save(); + assertTrue(eventCount.get().await(2, TimeUnit.SECONDS)); + + eventCount.set(new CountDownLatch(8)); + n.setProperty("property", 42); + n.addNode("3").setProperty("prop3", "val3"); + n1.setProperty("prop1", "val1 new"); + n1.getProperty("prop2").remove(); + n.getNode("2").remove(); + getAdminSession().save(); + assertTrue(eventCount.get().await(2, TimeUnit.SECONDS)); + + assertTrue("failedEvents not empty: " + failedEvents, failedEvents.isEmpty()); + assertTrue("addNodes not empty: " + addNodes, addNodes.isEmpty()); + assertTrue("removeNodes not empty: " + removeNodes, removeNodes.isEmpty()); + assertTrue("addProperties not empty: " + addProperties, addProperties.isEmpty()); + assertTrue("removeProperties not empty: " + removeProperties, removeProperties.isEmpty()); + assertTrue("setProperties not empty: " + setProperties, setProperties.isEmpty()); + } finally { + observingSession.logout(); + } + } + + @Test + public void observation2() throws RepositoryException, InterruptedException { + final Set addNodes = Sets.newHashSet( + TEST_PATH + "/1", + TEST_PATH + "/2"); + + final Set removeNodes = Sets.newHashSet( + TEST_PATH + "/1"); + + final Set addProperties = Sets.newHashSet( + TEST_PATH + "/1/jcr:primaryType", + TEST_PATH + "/2/jcr:primaryType"); + + final Set removeProperties = Sets.newHashSet( + TEST_PATH + "/1/jcr:primaryType"); + + final List failedEvents = new ArrayList(); + final AtomicReference eventCount = new AtomicReference(); + + final Session observingSession = createAnonymousSession(); + try { + ObservationManager obsMgr = observingSession.getWorkspace().getObservationManager(); + obsMgr.addEventListener(new EventListener() { + @Override + public void onEvent(EventIterator events) { + while (events.hasNext()) { + Event event = events.nextEvent(); + try { + String path = event.getPath(); + if (path.startsWith("/jcr:system")) { + // ignore changes in jcr:system + continue; + } + switch (event.getType()) { + case Event.NODE_ADDED: + if (!addNodes.remove(path)) { + failedEvents.add(event); + } + if (!observingSession.nodeExists(path)) { + failedEvents.add(event); + } + break; + case Event.NODE_REMOVED: + if (!removeNodes.remove(path)) { + failedEvents.add(event); + } + if (observingSession.nodeExists(path)) { + failedEvents.add(event); + } + break; + case Event.PROPERTY_ADDED: + if (!addProperties.remove(path)) { + failedEvents.add(event); + } + if (!observingSession.propertyExists(path)) { + failedEvents.add(event); + } + break; + case Event.PROPERTY_REMOVED: + if (!removeProperties.remove(path)) { + failedEvents.add(event); + } + if (observingSession.propertyExists(path)) { + failedEvents.add(event); + } + break; + default: + failedEvents.add(event); + } + } catch (RepositoryException e) { + failedEvents.add(event); + } + eventCount.get().countDown(); + } + } + }, + Event.NODE_ADDED | Event.NODE_REMOVED | Event.NODE_MOVED | Event.PROPERTY_ADDED | + Event.PROPERTY_REMOVED | Event.PROPERTY_CHANGED | Event.PERSIST, "/", true, null, null, false); + + eventCount.set(new CountDownLatch(2)); + Node n = getNode(TEST_PATH); + n.addNode("1"); + getAdminSession().save(); + assertTrue(eventCount.get().await(2, TimeUnit.SECONDS)); + + eventCount.set(new CountDownLatch(4)); + n.addNode("2"); + n.getNode("1").remove(); + getAdminSession().save(); + assertTrue(eventCount.get().await(2, TimeUnit.SECONDS)); + + assertTrue("failedEvents not empty: " + failedEvents, failedEvents.isEmpty()); + assertTrue("addNodes not empty: " + addNodes, addNodes.isEmpty()); + assertTrue("removeNodes not empty: " + removeNodes, removeNodes.isEmpty()); + assertTrue("addProperties not empty: " + addProperties, addProperties.isEmpty()); + assertTrue("removeProperties not empty: " + removeProperties, removeProperties.isEmpty()); + } finally { + observingSession.logout(); + } + } + + @Test + public void observationDispose() throws RepositoryException, InterruptedException, ExecutionException, + TimeoutException { + + final AtomicReference hasEvents = new AtomicReference(new CountDownLatch(1)); + final AtomicReference waitForRemove = new AtomicReference(new CountDownLatch(1)); + final Session observingSession = createAdminSession(); + try { + final ObservationManager obsMgr = observingSession.getWorkspace().getObservationManager(); + final EventListener listener = new EventListener() { + @Override + public void onEvent(EventIterator events) { + while (events.hasNext()) { + events.next(); + hasEvents.get().countDown(); + try { + // After receiving an event wait until event listener is removed + waitForRemove.get().await(); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + }; + + obsMgr.addEventListener(listener, Event.NODE_ADDED | Event.NODE_REMOVED | Event.NODE_MOVED | + Event.PROPERTY_ADDED | Event.PROPERTY_REMOVED | Event.PROPERTY_CHANGED | Event.PERSIST, + "/", true, null, null, false); + + // Generate two events + Node n = getNode(TEST_PATH); + n.setProperty("prop1", "val1"); + n.setProperty("prop2", "val2"); + n.getSession().save(); + + // Make sure we see the first event + assertTrue(hasEvents.get().await(2, TimeUnit.SECONDS)); + + // Remove event listener before it receives the second event + Executors.newSingleThreadExecutor().submit(new Callable() { + @Override + public Void call() throws Exception { + obsMgr.removeEventListener(listener); + return null; + } + }).get(2, TimeUnit.SECONDS); + hasEvents.set(new CountDownLatch(1)); + waitForRemove.get().countDown(); + + // Make sure we don't see the second event + assertFalse(hasEvents.get().await(2, TimeUnit.SECONDS)); + } + finally { + observingSession.logout(); + } + } + + @Test + public void liveNode() throws RepositoryException { + Session session = getAdminSession(); + + Node n1 = session.getNode(TEST_PATH); + Node n2 = session.getNode(TEST_PATH); + assertTrue(n1.isSame(n2)); + + Node c1 = n1.addNode("c1"); + n1.setProperty("p1", "v1"); + c1.setProperty("pc1", "vc1"); + + Node c2 = n2.addNode("c2"); + n2.setProperty("p2", "v2"); + c2.setProperty("pc2", "vc2"); + + assertTrue(c1.isSame(n2.getNode("c1"))); + assertTrue(c2.isSame(n1.getNode("c2"))); + + assertTrue(n1.hasNode("c1")); + assertTrue(n1.hasNode("c2")); + assertTrue(n1.hasProperty("p1")); + assertTrue(n1.hasProperty("p2")); + assertTrue(c1.hasProperty("pc1")); + assertFalse(c1.hasProperty("pc2")); + + assertTrue(n2.hasNode("c1")); + assertTrue(n2.hasNode("c2")); + assertTrue(n2.hasProperty("p1")); + assertTrue(n2.hasProperty("p2")); + assertFalse(c2.hasProperty("pc1")); + assertTrue(c2.hasProperty("pc2")); + } + + //------------------------------------------------------------< private >--- + + private Node getNode(String path) throws RepositoryException { + return getAdminSession().getNode(path); + } + + private Property getProperty(String path) throws RepositoryException { + return getAdminSession().getProperty(path); + } + + private void addProperty(Node parentNode, String name, Value value) throws RepositoryException, IOException { + String propertyPath = parentNode.getPath() + '/' + name; + assertFalse(getAdminSession().propertyExists(propertyPath)); + + Property added = parentNode.setProperty(name, value); + assertTrue(parentNode.isModified()); + assertFalse(added.isModified()); + assertTrue(added.isNew()); + getAdminSession().save(); + + Session session2 = createAnonymousSession(); + try { + assertTrue(session2.propertyExists(propertyPath)); + Value value2 = session2.getProperty(propertyPath).getValue(); + assertEquals(value.getType(), value2.getType()); + if (value.getType() == PropertyType.BINARY) { + assertEqualStream(value.getStream(), value2.getStream()); + } else { + assertEquals(value.getString(), value2.getString()); + } + + if (value2.getType() == PropertyType.REFERENCE || value2.getType() == PropertyType.WEAKREFERENCE) { + String ref = value2.getString(); + assertNotNull(getAdminSession().getNodeByIdentifier(ref)); + } + } finally { + session2.logout(); + } + } + + private static void assertEqualStream(InputStream is1, InputStream is2) throws IOException { + byte[] buf1 = new byte[65536]; + byte[] buf2 = new byte[65536]; + + int c = 0; + while (c != -1) { + assertEquals(c = is1.read(buf1), is2.read(buf2)); + for (int i = 0; i < c; i++) { + assertEquals(buf1[i], buf2[i]); + } + } + } + + /** + * Dummy stream class used by the binary property tests. + */ + private static class NumberStream extends InputStream { + + private final int limit; + + private int counter; + + public NumberStream(int limit) { + this.limit = limit; + } + + @Override + public int read() throws IOException { + return counter < limit + ? counter++ & 0xff + : -1; + } + + } + +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/TestContentLoader.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/TestContentLoader.java new file mode 100644 index 00000000000..7485aca4920 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/TestContentLoader.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Calendar; + +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.nodetype.NodeTypeManager; + +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.commons.cnd.ParseException; +import org.apache.jackrabbit.oak.plugins.nodetype.ReadWriteNodeTypeManager; +import org.apache.jackrabbit.value.BinaryValue; + +public class TestContentLoader { + + /** + * The encoding of the test resources. + */ + private static final String ENCODING = "UTF-8"; + + public void loadTestContent(Session session) throws RepositoryException, IOException, ParseException { + session.getWorkspace().getNamespaceRegistry().registerNamespace( + "test", "http://www.apache.org/jackrabbit/test"); + + registerTestNodeTypes(session.getWorkspace().getNodeTypeManager()); + + Node data = getOrAddNode(session.getRootNode(), "testdata"); + addPropertyTestData(getOrAddNode(data, "property")); + addQueryTestData(getOrAddNode(data, "query")); + addNodeTestData(getOrAddNode(data, "node")); + // TODO add lifecycle test data + // addLifecycleTestData(getOrAddNode(data, "lifecycle")); + addExportTestData(getOrAddNode(data, "docViewTest")); + + // TODO add retention test data + // Node conf = getOrAddNode(session.getRootNode(), "testconf"); + // addRetentionTestData(getOrAddNode(conf, "retentionTest")); + + session.save(); + } + + private static void registerTestNodeTypes(NodeTypeManager ntm) throws RepositoryException, ParseException, IOException { + InputStream stream = TestContentLoader.class.getResourceAsStream("test_nodetypes.cnd"); + try { + if (!(ntm instanceof ReadWriteNodeTypeManager)) { + throw new IllegalArgumentException("Need ReadWriteNodeTypeManager"); + } + ((ReadWriteNodeTypeManager)ntm).registerNodeTypes(new InputStreamReader(stream, "UTF-8")); + } finally { + stream.close(); + } + } + + private static Node getOrAddNode(Node node, String name) throws RepositoryException { + try { + return node.getNode(name); + } catch (PathNotFoundException e) { + return node.addNode(name); + } + } + + /** + * Creates a boolean, double, long, calendar and a path property at the + * given node. + */ + private static void addPropertyTestData(Node node) throws RepositoryException { + node.setProperty("boolean", true); + node.setProperty("double", Math.PI); + node.setProperty("long", 90834953485278298L); + Calendar c = Calendar.getInstance(); + c.set(2005, 6, 18, 17, 30); + node.setProperty("calendar", c); + ValueFactory factory = node.getSession().getValueFactory(); + node.setProperty("path", factory.createValue("/", PropertyType.PATH)); + node.setProperty("multi", new String[] { "one", "two", "three" }); + } + + // TODO add retention test data + /** + * Creates a node with a RetentionPolicy + */ + // private void addRetentionTestData(Node node) throws RepositoryException { + // RetentionPolicy rp = RetentionPolicyImpl.createRetentionPolicy("testRetentionPolicy", node.getSession()); + // node.getSession().getRetentionManager().setRetentionPolicy(node.getPath(), rp); + // } + + /** + * Creates four nodes under the given node. Each node has a String property + * named "prop1" with some content set. + */ + private static void addQueryTestData(Node node) throws RepositoryException { + while (node.hasNode("node1")) { + node.getNode("node1").remove(); + } + getOrAddNode(node, "node1").setProperty("prop1", "You can have it good, cheap, or fast. Any two."); + getOrAddNode(node, "node1").setProperty("prop1", "foo bar"); + getOrAddNode(node, "node1").setProperty("prop1", "Hello world!"); + getOrAddNode(node, "node2").setProperty("prop1", "Apache Jackrabbit"); + } + + /** + * Creates three nodes under the given node: one of type nt:resource and the + * other nodes referencing it. + */ + private static void addNodeTestData(Node node) throws RepositoryException, IOException { + if (node.hasNode("multiReference")) { + node.getNode("multiReference").remove(); + } + if (node.hasNode("resReference")) { + node.getNode("resReference").remove(); + } + if (node.hasNode("myResource")) { + node.getNode("myResource").remove(); + } + + Node resource = node.addNode("myResource", "nt:resource"); + // nt:resource not longer referenceable since JCR 2.0 + resource.addMixin("mix:referenceable"); + resource.setProperty("jcr:encoding", ENCODING); + resource.setProperty("jcr:mimeType", "text/plain"); + resource.setProperty("jcr:data", new BinaryValue("Hello w\u00F6rld.".getBytes(ENCODING))); + resource.setProperty("jcr:lastModified", Calendar.getInstance()); + + Node resReference = getOrAddNode(node, "reference"); + resReference.setProperty("ref", resource); + // make this node itself referenceable + resReference.addMixin("mix:referenceable"); + + Node multiReference = node.addNode("multiReference"); + ValueFactory factory = node.getSession().getValueFactory(); + multiReference.setProperty("ref", new Value[] { + factory.createValue(resource), + factory.createValue(resReference) + }); + + // NodeDefTest requires a test node with a mandatory child node + JcrUtils.putFile(node, "testFile", "text/plain", new ByteArrayInputStream("Hello, World!".getBytes("UTF-8"))); + } + + // TODO add lifecycle test data + /** + * Creates a lifecycle policy node and another node with a lifecycle + * referencing that policy. + */ + // private void addLifecycleTestData(Node node) throws RepositoryException { + // Node policy = getOrAddNode(node, "policy"); + // policy.addMixin(NodeType.MIX_REFERENCEABLE); + // Node transitions = getOrAddNode(policy, "transitions"); + // Node transition = getOrAddNode(transitions, "identity"); + // transition.setProperty("from", "identity"); + // transition.setProperty("to", "identity"); + // Node lifecycle = getOrAddNode(node, "node"); + // ((NodeImpl) lifecycle).assignLifecyclePolicy(policy, "identity"); + //} + + private static void addExportTestData(Node node) throws RepositoryException, IOException { + getOrAddNode(node, "invalidXmlName").setProperty("propName", "some text"); + + // three nodes which should be serialized as xml text in docView export + // separated with spaces + getOrAddNode(node, "jcr:xmltext").setProperty("jcr:xmlcharacters", "A text without any special character."); + getOrAddNode(node, "some-element"); + getOrAddNode(node, "jcr:xmltext").setProperty("jcr:xmlcharacters", + " The entity reference characters: <, ', ,&, >, \" should" + " be escaped in xml export. "); + getOrAddNode(node, "some-element"); + getOrAddNode(node, "jcr:xmltext").setProperty("jcr:xmlcharacters", "A text without any special character."); + + Node big = getOrAddNode(node, "bigNode"); + big.setProperty("propName0", "SGVsbG8gd8O2cmxkLg==;SGVsbG8gd8O2cmxkLg==".split(";"), PropertyType.BINARY); + big.setProperty("propName1", "text 1"); + big.setProperty("propName2", "multival text 1;multival text 2;multival text 3".split(";")); + big.setProperty("propName3", "text 1"); + + addExportValues(node, "propName"); + addExportValues(node, "Prop<>prop"); + } + + /** + * create nodes with following properties binary & single binary & multival + * notbinary & single notbinary & multival + */ + private static void addExportValues(Node node, String name) throws RepositoryException, IOException { + String prefix = "valid"; + if (name.indexOf('<') != -1) { + prefix = "invalid"; + } + node = getOrAddNode(node, prefix + "Names"); + + String[] texts = new String[] { "multival text 1", "multival text 2", "multival text 3" }; + getOrAddNode(node, prefix + "MultiNoBin").setProperty(name, texts); + + Node resource = getOrAddNode(node, prefix + "MultiBin"); + resource.setProperty("jcr:encoding", ENCODING); + resource.setProperty("jcr:mimeType", "text/plain"); + String[] values = new String[] { "SGVsbG8gd8O2cmxkLg==", "SGVsbG8gd8O2cmxkLg==" }; + resource.setProperty(name, values, PropertyType.BINARY); + resource.setProperty("jcr:lastModified", Calendar.getInstance()); + + getOrAddNode(node, prefix + "NoBin").setProperty(name, "text 1"); + + resource = getOrAddNode(node, "invalidBin"); + resource.setProperty("jcr:encoding", ENCODING); + resource.setProperty("jcr:mimeType", "text/plain"); + byte[] bytes = "Hello w\u00F6rld.".getBytes(ENCODING); + resource.setProperty(name, new BinaryValue(bytes)); + resource.setProperty("jcr:lastModified", Calendar.getInstance()); + } +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/ValueFactoryTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/ValueFactoryTest.java new file mode 100644 index 00000000000..be4395e42d4 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/ValueFactoryTest.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFactory; +import javax.jcr.ValueFormatException; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.fail; + +/** + * ValueFactoryTest... + */ +public class ValueFactoryTest extends AbstractRepositoryTest { + + private ValueFactory valueFactory; + + @Before + public void setup() throws RepositoryException { + valueFactory = getAdminSession().getValueFactory(); + } + + // TODO: this tests should be moved to the TCK. retrieving "invalidIdentifier" from the config. + + @Test + public void testReferenceValue() { + try { + valueFactory.createValue("invalidIdentifier", PropertyType.REFERENCE); + fail("Conversion to REFERENCE value must validate identifier string "); + } catch (ValueFormatException e) { + // success + } + } + + @Test + public void testWeakReferenceValue() { + try { + valueFactory.createValue("invalidIdentifier", PropertyType.WEAKREFERENCE); + fail("Conversion to WEAK_REFERENCE value must validate identifier string "); + } catch (ValueFormatException e) { + // success + } + } + +} \ No newline at end of file diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/PrefetchIteratorTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/PrefetchIteratorTest.java new file mode 100644 index 00000000000..a8bb75d11a6 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/PrefetchIteratorTest.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.query; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import org.junit.Test; + +/** + * Test the PrefetchIterator class. + */ +public class PrefetchIteratorTest { + + @Test + public void testKnownSize() { + Iterable s; + PrefetchIterator it; + s = seq(0, 100); + it = new PrefetchIterator(s.iterator(), 5, 0, 10, 200); + // reports the 'wrong' value as it was set manually + assertEquals(200, it.size()); + } + + @Test + public void testTimeout() { + Iterable s; + PrefetchIterator it; + + // long delay (10 ms per row) + long timeout = 10; + s = seq(0, 100, 10); + it = new PrefetchIterator(s.iterator(), 5, timeout, 1000, -1); + assertEquals(-1, it.size()); + + // no delay + s = seq(0, 100); + it = new PrefetchIterator(s.iterator(), 5, timeout, 1000, -1); + assertEquals(100, it.size()); + } + + @Test + public void test() { + // the following is the same as: + // for (int size = 0; size < 100; size++) + for (int size : seq(0, 100)) { + for (int readBefore : seq(0, 30)) { + // every 3th time, use a timeout + long timeout = size % 3 == 0 ? 100 : 0; + Iterable s = seq(0, size); + PrefetchIterator it = + new PrefetchIterator(s.iterator(), 20, timeout, 30, -1); + for (int x : seq(0, readBefore)) { + boolean hasNext = it.hasNext(); + if (!hasNext) { + assertEquals(x, size); + break; + } + String m = "s:" + size + " b:" + readBefore + " x:" + x; + assertTrue(m, hasNext); + assertEquals(m, x, it.next().intValue()); + } + String m = "s:" + size + " b:" + readBefore; + int max = timeout <= 0 ? 20 : 30; + if (size > max && readBefore <= size) { + assertEquals(m, -1, it.size()); + // calling it twice must not change the result + assertEquals(m, -1, it.size()); + } else { + assertEquals(m, size, it.size()); + // calling it twice must not change the result + assertEquals(m, size, it.size()); + } + for (int x : seq(readBefore, size)) { + m = "s:" + size + " b:" + readBefore + " x:" + x; + assertTrue(m, it.hasNext()); + assertEquals(m, x, it.next().intValue()); + } + assertFalse(it.hasNext()); + try { + it.next(); + fail(); + } catch (NoSuchElementException e) { + // expected + } + try { + it.remove(); + fail(); + } catch (UnsupportedOperationException e) { + // expected + } + } + } + } + + /** + * Create an integer sequence. + * + * @param start the first value + * @param limit the last value + 1 + * @return a sequence of the values [start .. limit-1] + */ + private static Iterable seq(final int start, final int limit) { + return seq(start, limit, 0); + } + + /** + * Create an integer sequence. + * + * @param start the first value + * @param limit the last value + 1 + * @param sleep the time to wait for each element + * @return a sequence of the values [start .. limit-1] + */ + private static Iterable seq(final int start, final int limit, final int sleep) { + return new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + int x = start; + @Override + public boolean hasNext() { + return x < limit; + } + @Override + public Integer next() { + if (sleep > 0) { + try { + Thread.sleep(sleep); + } catch (InterruptedException e) { + // ignore + } + } + return x++; + } + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + @Override + public String toString() { + return "[" + start + ".." + (limit - 1) + "]"; + } + }; + } + +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryTest.java new file mode 100644 index 00000000000..eaae8d0ab4f --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryTest.java @@ -0,0 +1,199 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import java.util.NoSuchElementException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.ValueFactory; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; +import javax.jcr.query.QueryResult; +import javax.jcr.query.Row; +import javax.jcr.query.RowIterator; +import org.apache.jackrabbit.oak.jcr.AbstractRepositoryTest; +import org.junit.Test; + +/** + * Tests the query feature. + */ +public class QueryTest extends AbstractRepositoryTest { + + @SuppressWarnings("deprecation") + @Test + public void simple() throws RepositoryException { + Session session = getAdminSession(); + Node hello = session.getRootNode().addNode("hello"); + hello.setProperty("id", "1"); + hello.setProperty("text", "hello_world"); + session.save(); + Node hello2 = session.getRootNode().addNode("hello2"); + hello2.setProperty("id", "2"); + hello2.setProperty("text", "hello world"); + session.save(); + + ValueFactory vf = session.getValueFactory(); + + QueryManager qm = session.getWorkspace().getQueryManager(); + + // SQL-2 + + Query q = qm.createQuery("select text from [nt:base] where id = $id", Query.JCR_SQL2); + q.bindValue("id", vf.createValue("1")); + QueryResult r = q.execute(); + RowIterator it = r.getRows(); + assertTrue(it.hasNext()); + Row row = it.nextRow(); + assertEquals("hello_world", row.getValue("text").getString()); + String[] columns = r.getColumnNames(); + assertEquals(1, columns.length); + assertEquals("text", columns[0]); + assertFalse(it.hasNext()); + + r = q.execute(); + NodeIterator nodeIt = r.getNodes(); + assertTrue(nodeIt.hasNext()); + Node n = nodeIt.nextNode(); + assertEquals("hello_world", n.getProperty("text").getString()); + assertFalse(it.hasNext()); + + // SQL + + q = qm.createQuery("select text from [nt:base] where text like 'hello\\_world' escape '\\'", Query.SQL); + r = q.execute(); + columns = r.getColumnNames(); + assertEquals(3, columns.length); + assertEquals("text", columns[0]); + assertEquals("jcr:path", columns[1]); + assertEquals("jcr:score", columns[2]); + nodeIt = r.getNodes(); + assertTrue(nodeIt.hasNext()); + n = nodeIt.nextNode(); + assertEquals("hello_world", n.getProperty("text").getString()); + assertFalse(nodeIt.hasNext()); + + // XPath + + q = qm.createQuery("//*[@id=1]", Query.XPATH); + r = q.execute(); + columns = r.getColumnNames(); + assertEquals(3, columns.length); + assertEquals("jcr:path", columns[0]); + assertEquals("jcr:score", columns[1]); + assertEquals("*", columns[2]); + } + + @Test + public void skip() throws RepositoryException { + Session session = getAdminSession(); + Node hello1 = session.getRootNode().addNode("hello1"); + hello1.setProperty("id", "1"); + hello1.setProperty("data", "x"); + session.save(); + Node hello3 = hello1.addNode("hello3"); + hello3.setProperty("id", "3"); + hello3.setProperty("data", "z"); + session.save(); + Node hello2 = hello3.addNode("hello2"); + hello2.setProperty("id", "2"); + hello2.setProperty("data", "y"); + session.save(); + ValueFactory vf = session.getValueFactory(); + QueryManager qm = session.getWorkspace().getQueryManager(); + Query q = qm.createQuery("select id from [nt:base] where data >= $data order by id", Query.JCR_SQL2); + q.bindValue("data", vf.createValue("x")); + for (int i = -1; i < 5; i++) { + QueryResult r = q.execute(); + RowIterator it = r.getRows(); + assertEquals(3, r.getRows().getSize()); + assertEquals(3, r.getNodes().getSize()); + Row row; + try { + it.skip(i); + assertTrue(i >= 0 && i <= 3); + } catch (IllegalArgumentException e) { + assertEquals(-1, i); + } catch (NoSuchElementException e) { + assertTrue(i >= 2); + } + if (i <= 0) { + assertTrue(it.hasNext()); + row = it.nextRow(); + assertEquals("1", row.getValue("id").getString()); + } + if (i <= 1) { + assertTrue(it.hasNext()); + row = it.nextRow(); + assertEquals("2", row.getValue("id").getString()); + } + if (i <= 2) { + assertTrue(it.hasNext()); + row = it.nextRow(); + assertEquals("3", row.getValue("id").getString()); + } + assertFalse(it.hasNext()); + } + } + + @Test + public void limit() throws RepositoryException { + Session session = getAdminSession(); + Node hello1 = session.getRootNode().addNode("hello1"); + hello1.setProperty("id", "1"); + hello1.setProperty("data", "x"); + session.save(); + Node hello3 = session.getRootNode().addNode("hello3"); + hello3.setProperty("id", "3"); + hello3.setProperty("data", "z"); + session.save(); + Node hello2 = session.getRootNode().addNode("hello2"); + hello2.setProperty("id", "2"); + hello2.setProperty("data", "y"); + session.save(); + ValueFactory vf = session.getValueFactory(); + QueryManager qm = session.getWorkspace().getQueryManager(); + Query q = qm.createQuery("select id from [nt:base] where data >= $data order by id", Query.JCR_SQL2); + q.bindValue("data", vf.createValue("x")); + for (int limit = 0; limit < 5; limit++) { + q.setLimit(limit); + for (int offset = 0; offset < 3; offset++) { + q.setOffset(offset); + QueryResult r = q.execute(); + RowIterator it = r.getRows(); + int l = Math.min(Math.max(0, 3 - offset), limit); + assertEquals(l, r.getRows().getSize()); + assertEquals(l, r.getNodes().getSize()); + Row row; + + for (int x = offset + 1, i = 0; i < limit && x < 4; i++, x++) { + assertTrue(it.hasNext()); + row = it.nextRow(); + assertEquals("" + x, row.getValue("id").getString()); + } + assertFalse(it.hasNext()); + } + } + } + +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/qom/QomTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/qom/QomTest.java new file mode 100644 index 00000000000..65f05f6d6a5 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/qom/QomTest.java @@ -0,0 +1,423 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.jcr.query.qom; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.nodetype.NodeType; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; +import javax.jcr.query.qom.And; +import javax.jcr.query.qom.BindVariableValue; +import javax.jcr.query.qom.ChildNode; +import javax.jcr.query.qom.ChildNodeJoinCondition; +import javax.jcr.query.qom.Column; +import javax.jcr.query.qom.Comparison; +import javax.jcr.query.qom.Constraint; +import javax.jcr.query.qom.DescendantNode; +import javax.jcr.query.qom.DescendantNodeJoinCondition; +import javax.jcr.query.qom.EquiJoinCondition; +import javax.jcr.query.qom.FullTextSearch; +import javax.jcr.query.qom.FullTextSearchScore; +import javax.jcr.query.qom.Join; +import javax.jcr.query.qom.Length; +import javax.jcr.query.qom.Literal; +import javax.jcr.query.qom.LowerCase; +import javax.jcr.query.qom.NodeLocalName; +import javax.jcr.query.qom.NodeName; +import javax.jcr.query.qom.Not; +import javax.jcr.query.qom.Or; +import javax.jcr.query.qom.Ordering; +import javax.jcr.query.qom.PropertyExistence; +import javax.jcr.query.qom.PropertyValue; +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.query.qom.QueryObjectModelFactory; +import javax.jcr.query.qom.SameNode; +import javax.jcr.query.qom.SameNodeJoinCondition; +import javax.jcr.query.qom.Selector; +import javax.jcr.query.qom.Source; +import javax.jcr.query.qom.UpperCase; + +import org.apache.jackrabbit.oak.jcr.AbstractRepositoryTest; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Tests the QueryObjectModelFactory and other QOM classes. + */ +public class QomTest extends AbstractRepositoryTest { + + private ValueFactory vf; + private QueryObjectModelFactory f; + + @Before + public void before() throws RepositoryException { + Session session = getAdminSession(); + vf = session.getValueFactory(); + QueryManager qm = session.getWorkspace().getQueryManager(); + f = qm.getQOMFactory(); + } + + @Test + public void jcrNameConversion() throws RepositoryException { + assertEquals("[nt:base]", + f.column(null, NodeType.NT_BASE, null).toString()); + assertEquals("[s1].[nt:base] = [s2].[nt:base]", + f.equiJoinCondition("s1", NodeType.NT_BASE, "s2", NodeType.NT_BASE).toString()); + assertEquals("CONTAINS([nt:base], null)", + f.fullTextSearch(null, NodeType.NT_BASE, null).toString()); + assertEquals("CAST('nt:base' AS NAME)", + f.literal(vf.createValue(NodeType.NT_BASE, PropertyType.NAME)).toString()); + assertEquals("[nt:base] IS NOT NULL", + f.propertyExistence(null, NodeType.NT_BASE).toString()); + assertEquals("[nt:base]", + f.propertyValue(null, NodeType.NT_BASE).toString()); + assertEquals("[nt:base]", + f.selector(NodeType.NT_BASE, null).toString()); + + Source source1 = f.selector(NodeType.NT_BASE, "selector"); + Column[] columns = new Column[] { f.column("selector", null, null) }; + Constraint constraint2 = f.childNode("selector", "/"); + QueryObjectModel qom = f.createQuery(source1, constraint2, null, + columns); + assertEquals("select [selector].* from " + + "[nt:base] AS [selector] " + + "where ISCHILDNODE([selector], [/])", qom.toString()); + } + + @Test + public void and() throws RepositoryException { + Constraint c0 = f.propertyExistence("x", "c0"); + Constraint c1 = f.propertyExistence("x", "c1"); + And and = f.and(c0, c1); + assertEquals(and.getConstraint1(), c0); + assertEquals(and.getConstraint2(), c1); + assertEquals("([x].[c0] IS NOT NULL) AND ([x].[c1] IS NOT NULL)", and.toString()); + } + + @Test + public void ascending() throws RepositoryException { + PropertyValue p = f.propertyValue("selectorName", "propertyName"); + Ordering o = f.ascending(p); + assertEquals(p, o.getOperand()); + assertEquals(QueryObjectModelConstants.JCR_ORDER_ASCENDING, o.getOrder()); + assertEquals("[selectorName].[propertyName]", p.toString()); + } + + @Test + public void bindVariable() throws RepositoryException { + BindVariableValue b = f.bindVariable("bindVariableName"); + assertEquals("bindVariableName", b.getBindVariableName()); + assertEquals("$bindVariableName", b.toString()); + } + + @Test + public void childNode() throws RepositoryException { + ChildNode cn = f.childNode("selectorName", "parentPath"); + assertEquals("selectorName", cn.getSelectorName()); + assertEquals("parentPath", cn.getParentPath()); + assertEquals("ISCHILDNODE([selectorName], [parentPath])", cn.toString()); + + assertEquals("ISCHILDNODE([p])", f.childNode(null, "p").toString()); + } + + @Test + public void childNodeJoinCondition() throws RepositoryException { + ChildNodeJoinCondition c = f.childNodeJoinCondition("childSelectorName", + "parentSelectorName"); + assertEquals("childSelectorName", c.getChildSelectorName()); + assertEquals("parentSelectorName", c.getParentSelectorName()); + assertEquals("ISCHILDNODE([childSelectorName], [parentSelectorName])", + c.toString()); + } + + @Test + public void column() throws RepositoryException { + Column c = f.column("selectorName", "propertyName", "columnName"); + assertEquals("selectorName", c.getSelectorName()); + assertEquals("propertyName", c.getPropertyName()); + assertEquals("columnName", c.getColumnName()); + assertEquals("[selectorName].[propertyName] AS [columnName]", c.toString()); + + assertEquals("[p]", f.column(null, "p", null).toString()); + assertEquals("[p] AS [c]", f.column(null, "p", "c").toString()); + assertEquals("[s].[p]", f.column("s", "p", null).toString()); + assertEquals("[s].[p] AS [c]", f.column("s", "p", "c").toString()); + assertEquals("[s].* AS [c]", f.column("s", null, "c").toString()); + assertEquals("* AS [c]", f.column(null, null, "c").toString()); + assertEquals("*", f.column(null, null, null).toString()); + assertEquals("[s].*", f.column("s", null, null).toString()); + } + + @Test + public void comparison() throws RepositoryException { + PropertyValue p = f.propertyValue("selectorName", "propertyName"); + Literal l = f.literal(vf.createValue(1)); + Comparison c = f.comparison(p, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, l); + assertEquals(p, c.getOperand1()); + assertEquals(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, c.getOperator()); + assertEquals(l, c.getOperand2()); + assertEquals("[selectorName].[propertyName] = 1", c.toString()); + } + + @Test + public void descendantNode() throws RepositoryException { + DescendantNode d = f.descendantNode("selectorName", "path"); + assertEquals("selectorName", d.getSelectorName()); + assertEquals("path", d.getAncestorPath()); + assertEquals("ISDESCENDANTNODE([selectorName], [path])", d.toString()); + + assertEquals("ISDESCENDANTNODE([p])", + f.descendantNode(null, "p").toString()); + } + + @Test + public void descendantNodeJoinCondition() throws RepositoryException { + DescendantNodeJoinCondition d = f.descendantNodeJoinCondition("descendantSelectorName", + "ancestorSelectorName"); + assertEquals("descendantSelectorName", d.getDescendantSelectorName()); + assertEquals("ancestorSelectorName", d.getAncestorSelectorName()); + assertEquals("ISDESCENDANTNODE([descendantSelectorName], [ancestorSelectorName])", + d.toString()); + } + + @Test + public void descending() throws RepositoryException { + PropertyValue p = f.propertyValue("selectorName", "propertyName"); + Ordering o = f.descending(p); + assertEquals(p, o.getOperand()); + assertEquals(QueryObjectModelConstants.JCR_ORDER_DESCENDING, o.getOrder()); + assertEquals("[selectorName].[propertyName] DESC", o.toString()); + } + + @Test + public void equiJoinCondition() throws RepositoryException { + EquiJoinCondition e = f.equiJoinCondition("selector1Name", "property1Name", + "selector2Name", "property2Name"); + assertEquals("selector1Name", e.getSelector1Name()); + assertEquals("property1Name", e.getProperty1Name()); + assertEquals("selector2Name", e.getSelector2Name()); + assertEquals("property2Name", e.getProperty2Name()); + assertEquals("[selector1Name].[property1Name] = [selector2Name].[property2Name]", + e.toString()); + } + + @Test + public void fullTextSearch() throws RepositoryException { + Literal l = f.literal(vf.createValue(1)); + FullTextSearch x = f.fullTextSearch("selectorName", "propertyName", l); + assertEquals("selectorName", x.getSelectorName()); + assertEquals("propertyName", x.getPropertyName()); + assertEquals(l, x.getFullTextSearchExpression()); + assertEquals("CONTAINS([selectorName].[propertyName], 1)", x.toString()); + + assertEquals("CONTAINS([p], null)", f.fullTextSearch(null, "p", null).toString()); + assertEquals("CONTAINS([s].[p], null)", f.fullTextSearch("s", "p", null).toString()); + assertEquals("CONTAINS([s].*, null)", f.fullTextSearch("s", null, null).toString()); + assertEquals("CONTAINS(*, null)", f.fullTextSearch(null, null, null).toString()); + } + + @Test + public void fullTextSearchScore() throws RepositoryException { + FullTextSearchScore x = f.fullTextSearchScore("selectorName"); + assertEquals("selectorName", x.getSelectorName()); + assertEquals("SCORE([selectorName])", x.toString()); + + assertEquals("SCORE()", f.fullTextSearchScore(null).toString()); + + } + + @Test + public void join() throws RepositoryException { + Source left = f.selector("nodeTypeName", "selectorName"); + Source right = f.selector("nodeTypeName2", "selectorName2"); + ChildNodeJoinCondition jc = f.childNodeJoinCondition("childSelectorName", "parentSelectorName"); + Join j = f.join(left, right, QueryObjectModelConstants.JCR_JOIN_TYPE_INNER, jc); + assertEquals(left, j.getLeft()); + assertEquals(right, j.getRight()); + assertEquals(QueryObjectModelConstants.JCR_JOIN_TYPE_INNER, j.getJoinType()); + assertEquals(jc, j.getJoinCondition()); + assertEquals("ISCHILDNODE([childSelectorName], [parentSelectorName])", jc.toString()); + } + + @Test + public void length() throws RepositoryException { + PropertyValue p = f.propertyValue("selectorName", "propertyName"); + Length l = f.length(p); + assertEquals(p, l.getPropertyValue()); + assertEquals("LENGTH([selectorName].[propertyName])", l.toString()); + } + + @Test + public void literal() throws RepositoryException { + Value v = vf.createValue(1); + Literal l = f.literal(v); + assertEquals(v, l.getLiteralValue()); + assertEquals("1", l.toString()); + assertEquals("'Joe''s'", f.literal(vf.createValue("Joe's")).toString()); + assertEquals("' - \" - '", f.literal(vf.createValue(" - \" - ")).toString()); + } + + @Test + public void lowerCase() throws RepositoryException { + PropertyValue p = f.propertyValue("selectorName", "propertyName"); + Length length = f.length(p); + LowerCase l = f.lowerCase(length); + assertEquals(length, l.getOperand()); + assertEquals("LOWER(LENGTH([selectorName].[propertyName]))", l.toString()); + } + + @Test + public void nodeLocalName() throws RepositoryException { + NodeLocalName n = f.nodeLocalName("selectorName"); + assertEquals("selectorName", n.getSelectorName()); + assertEquals("LOCALNAME([selectorName])", n.toString()); + assertEquals("LOCALNAME()", f.nodeLocalName(null).toString()); + } + + @Test + public void nodeName() throws RepositoryException { + NodeName n = f.nodeName("selectorName"); + assertEquals("selectorName", n.getSelectorName()); + assertEquals("NAME([selectorName])", n.toString()); + assertEquals("NAME()", f.nodeName(null).toString()); + } + + @Test + public void not() throws RepositoryException { + Constraint c = f.propertyExistence("x", "c0"); + Not n = f.not(c); + assertEquals(c, n.getConstraint()); + assertEquals("[x].[c0] IS NOT NULL", c.toString()); + + assertEquals("* IS NOT NULL", f.propertyExistence(null, null).toString()); + assertEquals("[s].* IS NOT NULL", f.propertyExistence("s", null).toString()); + assertEquals("[p] IS NOT NULL", f.propertyExistence(null, "p").toString()); + assertEquals("[s].[p] IS NOT NULL", f.propertyExistence("s", "p").toString()); + } + + @Test + public void or() throws RepositoryException { + Constraint c0 = f.propertyExistence("x", "c0"); + Constraint c1 = f.propertyExistence("x", "c1"); + Or or = f.or(c0, c1); + assertEquals(or.getConstraint1(), c0); + assertEquals(or.getConstraint2(), c1); + assertEquals("([x].[c0] IS NOT NULL) OR ([x].[c1] IS NOT NULL)", or.toString()); + } + + @Test + public void propertyExistence() throws RepositoryException { + PropertyExistence pe = f.propertyExistence("selectorName", "propertyName"); + assertEquals("selectorName", pe.getSelectorName()); + assertEquals("propertyName", pe.getPropertyName()); + assertEquals("[selectorName].[propertyName] IS NOT NULL", pe.toString()); + + assertEquals("* IS NOT NULL", + f.propertyExistence(null, null).toString()); + assertEquals("[s].* IS NOT NULL", + f.propertyExistence("s", null).toString()); + assertEquals("[p] IS NOT NULL", + f.propertyExistence(null, "p").toString()); + assertEquals("[s].[p] IS NOT NULL", + f.propertyExistence("s", "p").toString()); + } + + @Test + public void propertyValue() throws RepositoryException { + PropertyValue pv = f.propertyValue("selectorName", "propertyName"); + assertEquals("selectorName", pv.getSelectorName()); + assertEquals("propertyName", pv.getPropertyName()); + assertEquals("[selectorName].[propertyName]", pv.toString()); + + assertEquals("*", f.propertyValue(null, null).toString()); + assertEquals("[s].*", f.propertyValue("s", null).toString()); + assertEquals("[p]", f.propertyValue(null, "p").toString()); + assertEquals("[s].[p]", f.propertyValue("s", "p").toString()); + } + + @Test + public void sameNode() throws RepositoryException { + SameNode s = f.sameNode("selectorName", "path"); + assertEquals("selectorName", s.getSelectorName()); + assertEquals("path", s.getPath()); + assertEquals("ISSAMENODE([selectorName], [path])", s.toString()); + + assertEquals("ISSAMENODE([path])", f.sameNode(null, "path").toString()); + assertEquals("ISSAMENODE([s], [path])", f.sameNode("s", "path").toString()); + + } + + @Test + public void sameNodeJoinCondition() throws RepositoryException { + SameNodeJoinCondition s = f.sameNodeJoinCondition("selector1Name", "selector2Name", "selector2Path"); + assertEquals("selector1Name", s.getSelector1Name()); + assertEquals("selector2Name", s.getSelector2Name()); + assertEquals("selector2Path", s.getSelector2Path()); + assertEquals("ISSAMENODE([selector1Name], [selector2Name], [selector2Path])", + s.toString()); + } + + @Test + public void selector() throws RepositoryException { + Selector s = f.selector("nodeTypeName", "selectorName"); + assertEquals("nodeTypeName", s.getNodeTypeName()); + assertEquals("selectorName", s.getSelectorName()); + assertEquals("[nodeTypeName] AS [selectorName]", s.toString()); + assertEquals("[n]", f.selector("n", null).toString()); + } + + @Test + public void upperCase() throws RepositoryException { + PropertyValue p = f.propertyValue("selectorName", "propertyName"); + Length length = f.length(p); + UpperCase u = f.upperCase(length); + assertEquals(length, u.getOperand()); + assertEquals("UPPER(LENGTH([selectorName].[propertyName]))", u.toString()); + } + + @Test + public void createQuery() throws RepositoryException { + Selector s = f.selector("nodeTypeName", "x"); + BindVariableValue b = f.bindVariable("var"); + Constraint c = f.propertyExistence("x", "c"); + PropertyValue p = f.propertyValue("x", "propertyName"); + c = f.and(f.comparison(p, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, b), c); + Ordering o = f.ascending(p); + Column col = f.column("x", "propertyName", "columnName"); + Ordering[] ords = new Ordering[]{o}; + Column[] cols = new Column[]{col}; + QueryObjectModel q = f.createQuery(s, c, ords, cols); + assertEquals(Query.JCR_JQOM, q.getLanguage()); + String[] bv = q.getBindVariableNames(); + assertEquals(1, bv.length); + assertEquals("var", bv[0]); + assertEquals(s, q.getSource()); + assertEquals(c, q.getConstraint()); + assertEquals(o, q.getOrderings()[0]); + assertEquals(col, q.getColumns()[0]); + } + +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/principal/PrincipalManagerTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/principal/PrincipalManagerTest.java new file mode 100644 index 00000000000..451b1e234b2 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/principal/PrincipalManagerTest.java @@ -0,0 +1,354 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.security.principal; + +import java.security.Principal; +import java.security.acl.Group; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import javax.jcr.Credentials; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.junit.Before; +import org.junit.Test; + +/** + * {@code PrincipalManagerTest}... + */ +public class PrincipalManagerTest extends AbstractJCRTest { + + private PrincipalManager principalMgr; + private Group everyone; + + private Principal[] adminPrincipals; + + @Before + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!(superuser instanceof JackrabbitSession)) { + superuser.logout(); + throw new NotExecutableException(); + } + principalMgr = ((JackrabbitSession) superuser).getPrincipalManager(); + everyone = (Group) principalMgr.getEveryone(); + + adminPrincipals = getPrincipals(getHelper().getSuperuserCredentials()); + } + + private Principal[] getPrincipals(Credentials credentials) throws Exception { + // TODO: improve + Set principals = new HashSet(); + if (credentials instanceof SimpleCredentials) { + Principal p = principalMgr.getPrincipal(((SimpleCredentials) credentials).getUserID()); + if (p != null) { + principals.add(p); + PrincipalIterator principalIterator = principalMgr.getGroupMembership(p); + while (principalIterator.hasNext()) { + principals.add(principalIterator.nextPrincipal()); + } + } + } + return principals.toArray(new Principal[principals.size()]); + } + + private static boolean isGroup(Principal p) { + return p instanceof java.security.acl.Group; + } + + @Test + public void testGetEveryone() { + Principal principal = principalMgr.getEveryone(); + assertTrue(principal != null); + assertTrue(isGroup(principal)); + } + + /** + * @since oak + */ + @Test + public void testGetEveryoneByName() { + assertTrue(principalMgr.hasPrincipal(EveryonePrincipal.NAME)); + assertNotNull(principalMgr.getPrincipal(EveryonePrincipal.NAME)); + assertEquals(EveryonePrincipal.getInstance(), principalMgr.getPrincipal(EveryonePrincipal.NAME)); + } + + @Test + public void testSuperUserIsEveryOne() { + for (Principal pcpl : adminPrincipals) { + if (!(pcpl.equals(everyone))) { + assertTrue(everyone.isMember(pcpl)); + } + } + } + + @Test + public void testReadOnlyIsEveryOne() throws Exception { + Session s = getHelper().getReadOnlySession(); + try { + Principal[] pcpls = getPrincipals(getHelper().getReadOnlyCredentials()); + for (Principal pcpl : pcpls) { + if (!(pcpl.equals(everyone))) { + assertTrue(everyone.isMember(pcpl)); + } + } + } finally { + s.logout(); + } + } + + @Test + public void testHasPrincipal() { + assertTrue(principalMgr.hasPrincipal(everyone.getName())); + + for (Principal pcpl : adminPrincipals) { + assertTrue(principalMgr.hasPrincipal(pcpl.getName())); + } + } + + @Test + public void testGetPrincipal() { + Principal p = principalMgr.getPrincipal(everyone.getName()); + assertEquals(everyone, p); + + for (Principal pcpl : adminPrincipals) { + Principal pp = principalMgr.getPrincipal(pcpl.getName()); + assertEquals("PrincipalManager.getPrincipal returned Principal with different Name", pcpl.getName(), pp.getName()); + } + } + + @Test + public void testGetPrincipalGetName() { + for (Principal pcpl : adminPrincipals) { + Principal pp = principalMgr.getPrincipal(pcpl.getName()); + assertEquals("PrincipalManager.getPrincipal returned Principal with different Name", pcpl.getName(), pp.getName()); + } + } + + @Test + public void testGetPrincipals() { + PrincipalIterator it = principalMgr.getPrincipals(PrincipalManager.SEARCH_TYPE_NOT_GROUP); + while (it.hasNext()) { + Principal p = it.nextPrincipal(); + assertFalse(isGroup(p)); + } + } + + @Test + public void testGetGroupPrincipals() { + PrincipalIterator it = principalMgr.getPrincipals(PrincipalManager.SEARCH_TYPE_GROUP); + while (it.hasNext()) { + Principal p = it.nextPrincipal(); + assertTrue(isGroup(p)); + } + } + + @Test + public void testGetAllPrincipals() { + PrincipalIterator it = principalMgr.getPrincipals(PrincipalManager.SEARCH_TYPE_ALL); + while (it.hasNext()) { + Principal p = it.nextPrincipal(); + assertTrue(principalMgr.hasPrincipal(p.getName())); + assertEquals(principalMgr.getPrincipal(p.getName()), p); + } + } + + @Test + public void testGroupMembers() { + PrincipalIterator it = principalMgr.getPrincipals(PrincipalManager.SEARCH_TYPE_ALL); + while (it.hasNext()) { + Principal p = it.nextPrincipal(); + if (isGroup(p) && !p.equals(principalMgr.getEveryone())) { + Enumeration en = ((java.security.acl.Group) p).members(); + while (en.hasMoreElements()) { + Principal memb = en.nextElement(); + assertTrue(principalMgr.hasPrincipal(memb.getName())); + } + } + } + } + + @Test + public void testGroupMembership() { + testMembership(PrincipalManager.SEARCH_TYPE_NOT_GROUP); + testMembership(PrincipalManager.SEARCH_TYPE_GROUP); + testMembership(PrincipalManager.SEARCH_TYPE_ALL); + } + + private void testMembership(int searchType) { + PrincipalIterator it = principalMgr.getPrincipals(searchType); + while (it.hasNext()) { + Principal p = it.nextPrincipal(); + if (p.equals(everyone)) { + for (PrincipalIterator membership = principalMgr.getGroupMembership(p); membership.hasNext();) { + Principal gr = membership.nextPrincipal(); + assertTrue(isGroup(gr)); + if (gr.equals(everyone)) { + fail("Everyone must never be a member of the EveryOne group."); + } + } + } else { + boolean atleastEveryone = false; + for (PrincipalIterator membership = principalMgr.getGroupMembership(p); membership.hasNext();) { + Principal gr = membership.nextPrincipal(); + assertTrue(isGroup(gr)); + if (gr.equals(everyone)) { + atleastEveryone = true; + } + } + assertTrue("All principals (except everyone) must be member of the everyone group.", atleastEveryone); + + } + } + } + + @Test + public void testGetMembersConsistentWithMembership() { + Principal everyone = principalMgr.getEveryone(); + PrincipalIterator it = principalMgr.getPrincipals(PrincipalManager.SEARCH_TYPE_GROUP); + while (it.hasNext()) { + Principal p = it.nextPrincipal(); + if (p.equals(everyone)) { + continue; + } + + assertTrue(isGroup(p)); + + Enumeration members = ((java.security.acl.Group) p).members(); + while (members.hasMoreElements()) { + Principal memb = members.nextElement(); + + Principal group = null; + PrincipalIterator mship = principalMgr.getGroupMembership(memb); + while (mship.hasNext() && group == null) { + Principal gr = mship.nextPrincipal(); + if (p.equals(gr)) { + group = gr; + } + } + assertNotNull("Group member " + memb.getName() + "does not reveal group upon getGroupMembership", p.getName()); + } + } + } + + @Test + public void testFindPrincipal() { + for (Principal pcpl : adminPrincipals) { + if (pcpl.equals(everyone)) { + continue; + } + PrincipalIterator it = principalMgr.findPrincipals(pcpl.getName()); + // search must find at least a single principal + assertTrue("findPrincipals does not find principal with filter " + pcpl.getName(), it.hasNext()); + } + } + + @Test + public void testFindPrincipalByType() { + for (Principal pcpl : adminPrincipals) { + if (pcpl.equals(everyone)) { + // special case covered by another test + continue; + } + + if (isGroup(pcpl)) { + PrincipalIterator it = principalMgr.findPrincipals(pcpl.getName(), + PrincipalManager.SEARCH_TYPE_GROUP); + // search must find at least a single matching group principal + assertTrue("findPrincipals does not find principal with filter " + pcpl.getName(), it.hasNext()); + } else { + PrincipalIterator it = principalMgr.findPrincipals(pcpl.getName(), + PrincipalManager.SEARCH_TYPE_NOT_GROUP); + // search must find at least a single matching non-group principal + assertTrue("findPrincipals does not find principal with filter " + pcpl.getName(), it.hasNext()); + } + } + } + + @Test + public void testFindPrincipalByTypeAll() { + for (Principal pcpl : adminPrincipals) { + if (pcpl.equals(everyone)) { + // special case covered by another test + continue; + } + + PrincipalIterator it = principalMgr.findPrincipals(pcpl.getName(), PrincipalManager.SEARCH_TYPE_ALL); + PrincipalIterator it2 = principalMgr.findPrincipals(pcpl.getName()); + + // both search must reveal the same result and size + assertTrue(it.getSize() == it2.getSize()); + + Set s1 = new HashSet(); + Set s2 = new HashSet(); + while (it.hasNext() && it2.hasNext()) { + s1.add(it.nextPrincipal()); + s2.add(it2.nextPrincipal()); + } + + assertEquals(s1, s2); + assertFalse(it.hasNext() && it2.hasNext()); + } + } + + @Test + public void testFindEveryone() { + Principal everyone = principalMgr.getEveryone(); + + boolean containedInResult = false; + + // untyped search -> everyone must be part of the result set + PrincipalIterator it = principalMgr.findPrincipals(everyone.getName()); + while (it.hasNext()) { + Principal p = it.nextPrincipal(); + if (p.getName().equals(everyone.getName())) { + containedInResult = true; + } + } + assertTrue(containedInResult); + + // search group only -> everyone must be part of the result set + containedInResult = false; + it = principalMgr.findPrincipals(everyone.getName(), PrincipalManager.SEARCH_TYPE_GROUP); + while (it.hasNext()) { + Principal p = it.nextPrincipal(); + if (p.getName().equals(everyone.getName())) { + containedInResult = true; + } + } + assertTrue(containedInResult); + + // search non-group only -> everyone should not be part of the result set + containedInResult = false; + it = principalMgr.findPrincipals(everyone.getName(), PrincipalManager.SEARCH_TYPE_NOT_GROUP); + while (it.hasNext()) { + Principal p = it.nextPrincipal(); + if (p.getName().equals(everyone.getName())) { + containedInResult = true; + } + } + assertFalse(containedInResult); + } +} \ No newline at end of file diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/privilege/AbstractPrivilegeTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/privilege/AbstractPrivilegeTest.java new file mode 100644 index 00000000000..b80e7e4ad89 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/privilege/AbstractPrivilegeTest.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.jcr.security.privilege; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Workspace; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Base class for privilege management tests. + */ +abstract class AbstractPrivilegeTest extends AbstractJCRTest implements PrivilegeConstants { + + static PrivilegeManager getPrivilegeManager(Session session) throws RepositoryException { + Workspace workspace = session.getWorkspace(); + return ((JackrabbitWorkspace) workspace).getPrivilegeManager(); + } + + static String[] getAggregateNames(String... names) { + return names; + } + + static void assertContainsDeclared(Privilege privilege, String aggrName) { + boolean found = false; + for (Privilege p : privilege.getDeclaredAggregatePrivileges()) { + if (aggrName.equals(p.getName())) { + found = true; + break; + } + } + assertTrue(found); + } + + void assertPrivilege(Privilege priv, String name, boolean isAggregate, boolean isAbstract) { + assertNotNull(priv); + assertEquals(name, priv.getName()); + assertEquals(isAggregate, priv.isAggregate()); + assertEquals(isAbstract, priv.isAbstract()); + } +} \ No newline at end of file diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/privilege/PrivilegeManagerTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/privilege/PrivilegeManagerTest.java new file mode 100644 index 00000000000..2c35cd0a79b --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/privilege/PrivilegeManagerTest.java @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.security.privilege; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.jcr.RepositoryException; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * PrivilegeManagerTest... + */ +public class PrivilegeManagerTest extends AbstractPrivilegeTest { + + private PrivilegeManager privilegeManager; + + @Before + public void setUp() throws Exception { + super.setUp(); + privilegeManager = getPrivilegeManager(superuser); + } + + @After + public void tearDown() throws Exception { + privilegeManager = null; + super.tearDown(); + } + + @Test + public void testGetRegisteredPrivileges() throws RepositoryException { + Privilege[] registered = privilegeManager.getRegisteredPrivileges(); + Set set = new HashSet(); + Privilege all = privilegeManager.getPrivilege(Privilege.JCR_ALL); + set.add(all); + set.addAll(Arrays.asList(all.getAggregatePrivileges())); + + for (Privilege p : registered) { + assertTrue(set.remove(p)); + } + assertTrue(set.isEmpty()); + } + + @Test + public void testGetPrivilege() throws RepositoryException { + for (String privName : NON_AGGR_PRIVILEGES) { + Privilege p = privilegeManager.getPrivilege(privName); + assertPrivilege(p, privName, false, false); + } + + for (String privName : AGGR_PRIVILEGES) { + Privilege p = privilegeManager.getPrivilege(privName); + assertPrivilege(p, privName, true, false); + } + } + + @Test + public void testJcrAll() throws RepositoryException { + Privilege all = privilegeManager.getPrivilege(Privilege.JCR_ALL); + assertPrivilege(all, JCR_ALL, true, false); + + List decl = Arrays.asList(all.getDeclaredAggregatePrivileges()); + List aggr = new ArrayList(Arrays.asList(all.getAggregatePrivileges())); + + assertFalse(decl.contains(all)); + assertFalse(aggr.contains(all)); + + // declared and aggregated privileges are the same for jcr:all + assertTrue(decl.containsAll(aggr)); + + // test individual built-in privileges are listed in the aggregates + assertTrue(aggr.remove(privilegeManager.getPrivilege(Privilege.JCR_READ))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(Privilege.JCR_ADD_CHILD_NODES))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(Privilege.JCR_REMOVE_CHILD_NODES))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(Privilege.JCR_MODIFY_PROPERTIES))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(Privilege.JCR_REMOVE_NODE))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(Privilege.JCR_READ_ACCESS_CONTROL))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(Privilege.JCR_MODIFY_ACCESS_CONTROL))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(Privilege.JCR_LIFECYCLE_MANAGEMENT))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(Privilege.JCR_LOCK_MANAGEMENT))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(Privilege.JCR_NODE_TYPE_MANAGEMENT))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(Privilege.JCR_RETENTION_MANAGEMENT))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(Privilege.JCR_VERSION_MANAGEMENT))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(Privilege.JCR_WRITE))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(PrivilegeConstants.REP_WRITE))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(PrivilegeConstants.REP_READ_NODES))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(PrivilegeConstants.REP_READ_PROPERTIES))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(PrivilegeConstants.REP_ADD_PROPERTIES))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(PrivilegeConstants.REP_ALTER_PROPERTIES))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(PrivilegeConstants.REP_REMOVE_PROPERTIES))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(PrivilegeConstants.JCR_NAMESPACE_MANAGEMENT))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(PrivilegeConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(PrivilegeConstants.JCR_WORKSPACE_MANAGEMENT))); + assertTrue(aggr.remove(privilegeManager.getPrivilege(PrivilegeConstants.REP_PRIVILEGE_MANAGEMENT))); + + // there may be no privileges left + assertTrue(aggr.isEmpty()); + } + + @Test + public void testGetPrivilegeFromName() throws AccessControlException, RepositoryException { + Privilege p = privilegeManager.getPrivilege(Privilege.JCR_VERSION_MANAGEMENT); + + assertTrue(p != null); + assertEquals(PrivilegeConstants.JCR_VERSION_MANAGEMENT, p.getName()); + assertFalse(p.isAggregate()); + + p = privilegeManager.getPrivilege(Privilege.JCR_WRITE); + + assertTrue(p != null); + assertEquals(PrivilegeConstants.JCR_WRITE, p.getName()); + assertTrue(p.isAggregate()); + } + + @Test + public void testGetPrivilegesFromInvalidName() throws RepositoryException { + try { + privilegeManager.getPrivilege("unknown"); + fail("invalid privilege name"); + } catch (AccessControlException e) { + // OK + } + } + + @Test + public void testGetPrivilegesFromEmptyNames() { + try { + privilegeManager.getPrivilege(""); + fail("invalid privilege name array"); + } catch (AccessControlException e) { + // OK + } catch (RepositoryException e) { + // OK + } + } + + @Test + public void testGetPrivilegesFromNullNames() { + try { + privilegeManager.getPrivilege(null); + fail("invalid privilege name (null)"); + } catch (Exception e) { + // OK + } + } +} \ No newline at end of file diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/privilege/PrivilegeRegistrationTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/privilege/PrivilegeRegistrationTest.java new file mode 100644 index 00000000000..2b9490bdb38 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/privilege/PrivilegeRegistrationTest.java @@ -0,0 +1,402 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.security.privilege; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import javax.jcr.AccessDeniedException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.NamespaceException; +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Workspace; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +import org.apache.jackrabbit.oak.jcr.Jcr; +import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * CustomPrivilegeTest... + * + * TODO: more tests for cyclic aggregation + */ +public class PrivilegeRegistrationTest extends AbstractPrivilegeTest { + + private Repository repository; + private Session session; + private PrivilegeManager privilegeManager; + + @Before + public void setUp() throws Exception { + super.setUp(); + + // create a separate repository in order to be able to remove registered privileges. + String dir = "target/mk-tck-" + System.currentTimeMillis(); + repository = new Jcr(new MicroKernelImpl(dir)) + .with(Executors.newScheduledThreadPool(1)) + .createRepository(); + session = getAdminSession(); + privilegeManager = getPrivilegeManager(session); + + } + @After + public void tearDown() throws Exception { + try { + super.tearDown(); + } finally { + session.logout(); + repository = null; + privilegeManager = null; + } + } + + private Session getReadOnlySession() throws RepositoryException { + return repository.login(getHelper().getReadOnlyCredentials()); + } + + private Session getAdminSession() throws RepositoryException { + return repository.login(getHelper().getSuperuserCredentials()); + } + + @Test + public void testRegisterPrivilegeWithReadOnly() throws RepositoryException { + Session readOnly = getReadOnlySession(); + try { + getPrivilegeManager(readOnly).registerPrivilege("test", true, new String[0]); + fail("Only admin is allowed to register privileges."); + } catch (AccessDeniedException e) { + // success + } finally { + readOnly.logout(); + } + } + + @Test + public void testCustomDefinitionsWithCyclicReferences() throws RepositoryException { + try { + privilegeManager.registerPrivilege("cycl-1", false, new String[] {"cycl-1"}); + fail("Cyclic definitions must be detected upon registration."); + } catch (RepositoryException e) { + // success + } + } + + @Test + public void testCustomEquivalentDefinitions() throws RepositoryException { + privilegeManager.registerPrivilege("custom4", false, new String[0]); + privilegeManager.registerPrivilege("custom5", false, new String[0]); + privilegeManager.registerPrivilege("custom2", false, new String[]{"custom4", "custom5"}); + + List equivalent = new ArrayList(); + equivalent.add(new String[]{"custom4", "custom5"}); + equivalent.add(new String[] {"custom2", "custom4"}); + equivalent.add(new String[]{"custom2", "custom5"}); + int cnt = 6; + for (String[] aggrNames : equivalent) { + try { + // the equivalent definition to 'custom1' + String name = "custom"+(cnt++); + privilegeManager.registerPrivilege(name, false, aggrNames); + fail("Equivalent '"+name+"' definitions must be detected."); + } catch (RepositoryException e) { + // success + } + } + } + + @Test + public void testRegisterBuiltInPrivilege() throws RepositoryException { + Map builtIns = new HashMap(); + builtIns.put(PrivilegeConstants.JCR_READ, new String[0]); + builtIns.put(PrivilegeConstants.JCR_LIFECYCLE_MANAGEMENT, new String[] {PrivilegeConstants.JCR_ADD_CHILD_NODES}); + builtIns.put(PrivilegeConstants.REP_WRITE, new String[0]); + builtIns.put(PrivilegeConstants.JCR_ALL, new String[0]); + + for (String builtInName : builtIns.keySet()) { + try { + privilegeManager.registerPrivilege(builtInName, false, builtIns.get(builtInName)); + fail("Privilege name " +builtInName+ " already in use -> Exception expected"); + } catch (RepositoryException e) { + // success + } + } + } + + @Test + public void testRegisterInvalidNewAggregate() throws RepositoryException { + Map newAggregates = new LinkedHashMap(); + // same as jcr:read + newAggregates.put("jcrReadAggregate", getAggregateNames(PrivilegeConstants.JCR_READ)); + // aggregated combining built-in and an unknown privilege + newAggregates.put("newAggregate2", getAggregateNames(PrivilegeConstants.JCR_READ, "unknownPrivilege")); + // aggregate containing unknown privilege + newAggregates.put("newAggregate3", getAggregateNames("unknownPrivilege")); + // custom aggregated contains itself + newAggregates.put("newAggregate4", getAggregateNames("newAggregate")); + // same as rep:write + newAggregates.put("repWriteAggregate", getAggregateNames(PrivilegeConstants.JCR_MODIFY_PROPERTIES, PrivilegeConstants.JCR_ADD_CHILD_NODES, PrivilegeConstants.JCR_NODE_TYPE_MANAGEMENT, PrivilegeConstants.JCR_REMOVE_CHILD_NODES, PrivilegeConstants.JCR_REMOVE_NODE)); + // aggregated combining built-in and unknown custom + newAggregates.put("newAggregate5", getAggregateNames(PrivilegeConstants.JCR_READ, "unknownPrivilege")); + + for (String name : newAggregates.keySet()) { + try { + privilegeManager.registerPrivilege(name, true, newAggregates.get(name)); + fail("New aggregate "+ name +" referring to unknown Privilege -> Exception expected"); + } catch (RepositoryException e) { + // success + } + } + } + + @Test + public void testRegisterInvalidNewAggregate2() throws RepositoryException { + Map newCustomPrivs = new LinkedHashMap(); + newCustomPrivs.put("new", new String[0]); + newCustomPrivs.put("new2", new String[0]); + newCustomPrivs.put("new3", getAggregateNames("new", "new2")); + + for (String name : newCustomPrivs.keySet()) { + boolean isAbstract = true; + String[] aggrNames = newCustomPrivs.get(name); + privilegeManager.registerPrivilege(name, isAbstract, aggrNames); + } + + Map newAggregates = new LinkedHashMap(); + // other illegal aggregates already represented by registered definition. + newAggregates.put("newA2", getAggregateNames("new")); + newAggregates.put("newA3", getAggregateNames("new2")); + + for (String name : newAggregates.keySet()) { + boolean isAbstract = false; + String[] aggrNames = newAggregates.get(name); + + try { + privilegeManager.registerPrivilege(name, isAbstract, aggrNames); + fail("Invalid aggregation in definition '"+ name.toString()+"' : Exception expected"); + } catch (RepositoryException e) { + // success + } + } + } + + @Test + public void testRegisterPrivilegeWithIllegalName() { + Map illegal = new HashMap(); + // invalid privilege name + illegal.put(null, new String[0]); + illegal.put("", new String[0]); + illegal.put("invalid:privilegeName", new String[0]); + illegal.put(".e:privilegeName", new String[0]); + // invalid aggregate names + illegal.put("newPrivilege", new String[] {"invalid:privilegeName"}); + illegal.put("newPrivilege", new String[] {".e:privilegeName"}); + illegal.put("newPrivilege", new String[] {null}); + illegal.put("newPrivilege", new String[] {""}); + + for (String illegalName : illegal.keySet()) { + try { + privilegeManager.registerPrivilege(illegalName, true, illegal.get(illegalName)); + fail("Illegal name -> Exception expected"); + } catch (NamespaceException e) { + // success + } catch (RepositoryException e) { + // success + } + } + } + + @Test + public void testRegisterReservedName() { + Map illegal = new HashMap(); + // invalid privilege name + illegal.put(null, new String[0]); + illegal.put("jcr:privilegeName", new String[0]); + illegal.put("rep:privilegeName", new String[0]); + illegal.put("nt:privilegeName", new String[0]); + illegal.put("mix:privilegeName", new String[0]); + illegal.put("sv:privilegeName", new String[0]); + illegal.put("xml:privilegeName", new String[0]); + illegal.put("xmlns:privilegeName", new String[0]); + // invalid aggregate names + illegal.put("newPrivilege", new String[] {"jcr:privilegeName"}); + + for (String illegalName : illegal.keySet()) { + try { + privilegeManager.registerPrivilege(illegalName, true, illegal.get(illegalName)); + fail("Illegal name -> Exception expected"); + } catch (RepositoryException e) { + // success + } + } + } + + @Test + public void testRegisterCustomPrivileges() throws RepositoryException { + Workspace workspace = session.getWorkspace(); + workspace.getNamespaceRegistry().registerNamespace("test", "http://www.apache.org/jackrabbit/test"); + + Map newCustomPrivs = new HashMap(); + newCustomPrivs.put("new", new String[0]); + newCustomPrivs.put("test:new", new String[0]); + + for (String name : newCustomPrivs.keySet()) { + boolean isAbstract = true; + String[] aggrNames = newCustomPrivs.get(name); + + Privilege registered = privilegeManager.registerPrivilege(name, isAbstract, aggrNames); + + // validate definition + Privilege privilege = privilegeManager.getPrivilege(name); + assertNotNull(privilege); + assertEquals(name, privilege.getName()); + assertTrue(privilege.isAbstract()); + assertEquals(0, privilege.getDeclaredAggregatePrivileges().length); + assertContainsDeclared(privilegeManager.getPrivilege(PrivilegeConstants.JCR_ALL), name); + } + + Map newAggregates = new HashMap(); + // a new aggregate of custom privileges + newAggregates.put("newA2", getAggregateNames("test:new", "new")); + // a new aggregate of custom and built-in privilege + newAggregates.put("newA1", getAggregateNames("new", PrivilegeConstants.JCR_READ)); + // aggregating built-in privileges + newAggregates.put("aggrBuiltIn", getAggregateNames(PrivilegeConstants.JCR_MODIFY_PROPERTIES, PrivilegeConstants.JCR_READ)); + + for (String name : newAggregates.keySet()) { + boolean isAbstract = false; + String[] aggrNames = newAggregates.get(name); + privilegeManager.registerPrivilege(name, isAbstract, aggrNames); + Privilege p = privilegeManager.getPrivilege(name); + + assertNotNull(p); + assertEquals(name, p.getName()); + assertFalse(p.isAbstract()); + + for (String n : aggrNames) { + assertContainsDeclared(p, n); + } + assertContainsDeclared(privilegeManager.getPrivilege(PrivilegeConstants.JCR_ALL), name); + } + } + + /** + * @since oak + */ + @Test + public void testRegisterCustomPrivilegesVisibleInContent() throws RepositoryException { + Workspace workspace = session.getWorkspace(); + workspace.getNamespaceRegistry().registerNamespace("test", "http://www.apache.org/jackrabbit/test"); + + Map newCustomPrivs = new HashMap(); + newCustomPrivs.put("new", new String[0]); + newCustomPrivs.put("test:new", new String[0]); + + for (String name : newCustomPrivs.keySet()) { + boolean isAbstract = true; + String[] aggrNames = newCustomPrivs.get(name); + + Privilege registered = privilegeManager.registerPrivilege(name, isAbstract, aggrNames); + + Node privilegeRoot = session.getNode(PrivilegeConstants.PRIVILEGES_PATH); + assertTrue(privilegeRoot.hasNode(name)); + Node privNode = privilegeRoot.getNode(name); + assertTrue(privNode.getProperty(PrivilegeConstants.REP_IS_ABSTRACT).getBoolean()); + assertFalse(privNode.hasProperty(PrivilegeConstants.REP_AGGREGATES)); + } + } + + /** + * @since oak + */ + @Test + public void testCustomPrivilegeVisibleToNewSession() throws RepositoryException { + boolean isAbstract = false; + String privName = "testCustomPrivilegeVisibleToNewSession"; + privilegeManager.registerPrivilege(privName, isAbstract, new String[0]); + + Session s2 = getAdminSession(); + try { + PrivilegeManager pm = getPrivilegeManager(s2); + Privilege priv = pm.getPrivilege(privName); + assertEquals(privName, priv.getName()); + assertEquals(isAbstract, priv.isAbstract()); + assertFalse(priv.isAggregate()); + } finally { + s2.logout(); + } + } + + /** + * @since oak + */ + @Test + public void testCustomPrivilegeVisibleAfterRefresh() throws RepositoryException { + Session s2 = getAdminSession(); + PrivilegeManager pm = getPrivilegeManager(s2); + try { + boolean isAbstract = false; + String privName = "testCustomPrivilegeVisibleAfterRefresh"; + privilegeManager.registerPrivilege(privName, isAbstract, new String[0]); + + // before refreshing: privilege not visible + try { + Privilege priv = pm.getPrivilege(privName); + fail("Custom privilege will show up after Session#refresh()"); + } catch (AccessControlException e) { + // success + } + + // latest after refresh privilege manager must be updated + s2.refresh(true); + Privilege priv = pm.getPrivilege(privName); + assertEquals(privName, priv.getName()); + assertEquals(isAbstract, priv.isAbstract()); + assertFalse(priv.isAggregate()); + } finally { + s2.logout(); + } + } + + /** + * @since oak + */ + @Test + public void testRegisterPrivilegeWithPendingChanges() throws RepositoryException { + try { + session.getRootNode().addNode("test"); + assertTrue(session.hasPendingChanges()); + privilegeManager.registerPrivilege("new", true, new String[0]); + fail("Privileges may not be registered while there are pending changes."); + } catch (InvalidItemStateException e) { + // success + } finally { + superuser.refresh(false); + } + } +} \ No newline at end of file diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/AbstractUserTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/AbstractUserTest.java new file mode 100644 index 00000000000..ac9eb16b398 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/AbstractUserTest.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.security.user; + +import java.security.Principal; +import java.util.Collections; +import java.util.UUID; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.security.auth.Subject; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.junit.After; +import org.junit.Before; + +/** + * Base class for user mgt related tests + */ +public abstract class AbstractUserTest extends AbstractJCRTest { + + protected String testPw = "pw"; + + protected UserManager userMgr; + protected User user; + protected Group group; + + @Before + @Override + protected void setUp() throws Exception { + super.setUp(); + + userMgr = getUserManager(superuser); + + user = userMgr.createUser(createUserId(), testPw); + group = userMgr.createGroup(createGroupId()); + superuser.save(); + } + + @After + @Override + protected void tearDown() throws Exception { + try { + if (user != null) { + user.remove(); + } + if (group != null) { + group.remove(); + } + superuser.save(); + } finally { + super.tearDown(); + } + } + + protected static UserManager getUserManager(Session session) throws RepositoryException, NotExecutableException { + if (!(session instanceof JackrabbitSession)) { + throw new NotExecutableException(); + } + try { + return ((JackrabbitSession) session).getUserManager(); + } catch (UnsupportedRepositoryOperationException e) { + throw new NotExecutableException(e.getMessage()); + } catch (UnsupportedOperationException e) { + throw new NotExecutableException(e.getMessage()); + } + } + + protected static Subject buildSubject(Principal p) { + return new Subject(true, Collections.singleton(p), Collections.emptySet(), Collections.emptySet()); + } + + protected static Node getNode(Authorizable authorizable, Session session) throws NotExecutableException, RepositoryException { + String path = authorizable.getPath(); + if (session.nodeExists(path)) { + return session.getNode(path); + } else { + throw new NotExecutableException("Cannot access node for authorizable " + authorizable.getID()); + } + } + + protected String createUserId() throws RepositoryException { + return "testUser_" + UUID.randomUUID(); + } + + protected String createGroupId() throws RepositoryException { + return "testGroup_" + UUID.randomUUID(); + } + + protected Principal getTestPrincipal() throws RepositoryException { + String pn = "testPrincipal_" + UUID.randomUUID(); + return getTestPrincipal(pn); + } + + protected Principal getTestPrincipal(final String name) throws RepositoryException { + return new Principal() { + + @Override + public String getName() { + return name; + } + }; + } + + protected User getTestUser(Session session) throws NotExecutableException, RepositoryException { + Authorizable auth = getUserManager(session).getAuthorizable(session.getUserID()); + if (auth != null && !auth.isGroup()) { + return (User) auth; + } + // should never happen. An Session should always have a corresponding User. + throw new NotExecutableException("Unable to retrieve a User."); + } + +} \ No newline at end of file diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/AdministratorTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/AdministratorTest.java new file mode 100644 index 00000000000..ddce329c4a5 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/AdministratorTest.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.security.user; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.test.NotExecutableException; +import org.junit.Before; +import org.junit.Test; + +/** + * AdministratorTest... + */ +public class AdministratorTest extends AbstractUserTest { + + private User admin; + + @Before + @Override + protected void setUp() throws Exception { + super.setUp(); + + Authorizable a = userMgr.getAuthorizable(superuser.getUserID()); + if (a == null || a.isGroup()) { + throw new NotExecutableException("Admin user does not exist"); + } + admin = (User) a; + } + + @Test + public void testIsAdmin() throws NotExecutableException, RepositoryException { + assertTrue(admin.isAdmin()); + } + + @Test + public void testDisable() throws NotExecutableException, RepositoryException { + try { + admin.disable("-> out"); + superuser.save(); + fail("The admin cannot be disabled"); + } catch (RepositoryException e) { + // success + } + } + + @Test + public void testRemoveAdmin() throws NotExecutableException { + try { + admin.remove(); + superuser.save(); + fail("The admin user cannot be removed."); + } catch (RepositoryException e) { + // OK superuser cannot be removed. not even by the superuser itself. + } + } + + @Test + public void testRemoveAdminNode() throws RepositoryException, NotExecutableException { + String adminId = admin.getID(); + // access the node corresponding to the admin user and remove it + Node adminNode = superuser.getNode(admin.getPath()); + + try { + adminNode.remove(); + // use session obtained from the node as usermgr may point to a dedicated + // system workspace different from the superusers workspace. + superuser.save(); + fail("Admin user node cannot be removed."); + } catch (Exception e) { + // success -> get rid of possibly pending transient modifications + superuser.refresh(false); + } + } +} \ No newline at end of file diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/AuthorizableTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/AuthorizableTest.java new file mode 100644 index 00000000000..3b67e4ad937 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/AuthorizableTest.java @@ -0,0 +1,753 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.security.user; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.value.StringValue; +import org.junit.Test; + +/** + * AuthorizableTest... + */ +public class AuthorizableTest extends AbstractUserTest { + + private Map protectedUserProps = new HashMap(); + private Map protectedGroupProps = new HashMap(); + + @Override + protected void setUp() throws Exception { + super.setUp(); + + protectedUserProps.put(UserConstants.REP_PASSWORD, false); + protectedUserProps.put(UserConstants.REP_IMPERSONATORS, true); + protectedUserProps.put(UserConstants.REP_PRINCIPAL_NAME, false); + + protectedGroupProps.put(UserConstants.REP_MEMBERS, true); + protectedGroupProps.put(UserConstants.REP_PRINCIPAL_NAME, false); + } + + private static void checkProtected(Property prop) throws RepositoryException { + assertTrue(prop.getDefinition().isProtected()); + } + + @Test + public void testSetProperty() throws NotExecutableException, RepositoryException { + Authorizable auth = getTestUser(superuser); + + String propName = "Fullname"; + Value v = superuser.getValueFactory().createValue("Super User"); + try { + auth.setProperty(propName, v); + superuser.save(); + } catch (RepositoryException e) { + throw new NotExecutableException("Cannot test 'Authorizable.setProperty'."); + } + + try { + boolean found = false; + for (Iterator it = auth.getPropertyNames(); it.hasNext() && !found;) { + found = propName.equals(it.next()); + } + assertTrue(found); + + found = false; + for (Iterator it = auth.getPropertyNames("."); it.hasNext() && !found;) { + found = propName.equals(it.next()); + } + assertTrue(found); + + assertTrue(auth.hasProperty(propName)); + assertTrue(auth.hasProperty("./" + propName)); + + assertTrue(auth.getProperty(propName).length == 1); + + assertEquals(v, auth.getProperty(propName)[0]); + assertEquals(v, auth.getProperty("./" + propName)[0]); + + assertTrue(auth.removeProperty(propName)); + assertFalse(auth.hasProperty(propName)); + + superuser.save(); + } finally { + // try to remove the property again even if previous calls failed. + auth.removeProperty(propName); + superuser.save(); + } + } + + @Test + public void testSetMultiValueProperty() throws NotExecutableException, RepositoryException { + Authorizable auth = getTestUser(superuser); + + String propName = "Fullname"; + Value[] v = new Value[] {superuser.getValueFactory().createValue("Super User")}; + try { + auth.setProperty(propName, v); + superuser.save(); + } catch (RepositoryException e) { + throw new NotExecutableException("Cannot test 'Authorizable.setProperty'."); + } + + try { + boolean found = false; + for (Iterator it = auth.getPropertyNames(); it.hasNext() && !found;) { + found = propName.equals(it.next()); + } + assertTrue(found); + + found = false; + for (Iterator it = auth.getPropertyNames("."); it.hasNext() && !found;) { + found = propName.equals(it.next()); + } + assertTrue(found); + + assertTrue(auth.hasProperty(propName)); + assertTrue(auth.hasProperty("./" + propName)); + + assertEquals(Arrays.asList(v), Arrays.asList(auth.getProperty(propName))); + assertEquals(Arrays.asList(v), Arrays.asList(auth.getProperty("./" + propName))); + + assertTrue(auth.removeProperty(propName)); + assertFalse(auth.hasProperty(propName)); + + superuser.save(); + } finally { + // try to remove the property again even if previous calls failed. + auth.removeProperty(propName); + superuser.save(); + } + } + + @Test + public void testSetPropertyByRelPath() throws NotExecutableException, RepositoryException { + Authorizable auth = getTestUser(superuser); + Value[] v = new Value[] {superuser.getValueFactory().createValue("Super User")}; + + List relPaths = new ArrayList(); + relPaths.add("testing/Fullname"); + relPaths.add("testing/Email"); + relPaths.add("testing/testing/testing/Fullname"); + relPaths.add("testing/testing/testing/Email"); + + for (String relPath : relPaths) { + try { + auth.setProperty(relPath, v); + superuser.save(); + + assertTrue(auth.hasProperty(relPath)); + String propName = Text.getName(relPath); + assertFalse(auth.hasProperty(propName)); + } finally { + // try to remove the property even if previous calls failed. + auth.removeProperty(relPath); + superuser.save(); + } + } + } + + @Test + public void testSetPropertyInvalidRelativePath() throws NotExecutableException, RepositoryException { + Authorizable auth = getTestUser(superuser); + Value[] v = new Value[] {superuser.getValueFactory().createValue("Super User")}; + + List invalidPaths = new ArrayList(); + // try setting outside of tree defined by the user. + invalidPaths.add("../testing/Fullname"); + invalidPaths.add("../../testing/Fullname"); + invalidPaths.add("testing/testing/../../../Fullname"); + // try absolute path -> must fail + invalidPaths.add("/testing/Fullname"); + + for (String invalidRelPath : invalidPaths) { + try { + auth.setProperty(invalidRelPath, v); + fail("Modifications outside of the scope of the authorizable must fail. Path was: " + invalidRelPath); + } catch (Exception e) { + // success. + } finally { + superuser.refresh(false); + } + } + } + + @Test + public void testGetPropertyByInvalidRelativePath() throws NotExecutableException, RepositoryException { + Authorizable auth = getTestUser(superuser); + + List wrongPaths = new ArrayList(); + wrongPaths.add("../jcr:primaryType"); + wrongPaths.add("../../jcr:primaryType"); + wrongPaths.add("../testing/jcr:primaryType"); + for (String path : wrongPaths) { + assertNull(auth.getProperty(path)); + } + + List invalidPaths = new ArrayList(); + invalidPaths.add("/testing/jcr:primaryType"); + invalidPaths.add(".."); + invalidPaths.add("."); + invalidPaths.add(null); + for (String invalidPath : invalidPaths) { + try { + assertNull(auth.getProperty(invalidPath)); + } catch (Exception e) { + // success + } + } + } + + @Test + public void testHasPropertyByInvalidRelativePath() throws NotExecutableException, RepositoryException { + Authorizable auth = getTestUser(superuser); + + List wrongPaths = new ArrayList(); + wrongPaths.add("../jcr:primaryType"); + wrongPaths.add("../../jcr:primaryType"); + wrongPaths.add("../testing/jcr:primaryType"); + for (String path : wrongPaths) { + assertFalse(auth.hasProperty(path)); + } + + + List invalidPaths = new ArrayList(); + invalidPaths.add(".."); + invalidPaths.add("."); + invalidPaths.add(null); + + for (String invalidPath : invalidPaths) { + try { + assertFalse(auth.hasProperty(invalidPath)); + } catch (Exception e) { + // success + } + } + } + + @Test + public void testGetPropertyNames() throws NotExecutableException, RepositoryException { + Authorizable auth = getTestUser(superuser); + + String propName = "Fullname"; + Value v = superuser.getValueFactory().createValue("Super User"); + try { + auth.setProperty(propName, v); + superuser.save(); + } catch (RepositoryException e) { + throw new NotExecutableException("Cannot test 'Authorizable.setProperty'."); + } + + try { + for (Iterator it = auth.getPropertyNames(); it.hasNext();) { + String name = it.next(); + assertTrue(auth.hasProperty(name)); + assertNotNull(auth.getProperty(name)); + } + } finally { + // try to remove the property again even if previous calls failed. + auth.removeProperty(propName); + superuser.save(); + } + } + + @Test + public void testGetPropertyNamesByRelPath() throws NotExecutableException, RepositoryException { + Authorizable auth = getTestUser(superuser); + + String relPath = "testing/Fullname"; + Value v = superuser.getValueFactory().createValue("Super User"); + try { + auth.setProperty(relPath, v); + superuser.save(); + } catch (RepositoryException e) { + throw new NotExecutableException("Cannot test 'Authorizable.setProperty'."); + } + + try { + for (Iterator it = auth.getPropertyNames(); it.hasNext();) { + String name = it.next(); + assertFalse("Fullname".equals(name)); + } + + for (Iterator it = auth.getPropertyNames("testing"); it.hasNext();) { + String name = it.next(); + String rp = "testing/" + name; + + assertFalse(auth.hasProperty(name)); + assertNull(auth.getProperty(name)); + + assertTrue(auth.hasProperty(rp)); + assertNotNull(auth.getProperty(rp)); + } + for (Iterator it = auth.getPropertyNames("./testing"); it.hasNext();) { + String name = it.next(); + String rp = "testing/" + name; + + assertFalse(auth.hasProperty(name)); + assertNull(auth.getProperty(name)); + + assertTrue(auth.hasProperty(rp)); + assertNotNull(auth.getProperty(rp)); + } + } finally { + // try to remove the property again even if previous calls failed. + auth.removeProperty(relPath); + superuser.save(); + } + } + + @Test + public void testGetPropertyNamesByInvalidRelPath() throws NotExecutableException, RepositoryException { + Authorizable auth = getTestUser(superuser); + + List invalidPaths = new ArrayList(); + invalidPaths.add("../"); + invalidPaths.add("../../"); + invalidPaths.add("../testing"); + invalidPaths.add("/testing"); + invalidPaths.add(null); + + for (String invalidRelPath : invalidPaths) { + try { + auth.getPropertyNames(invalidRelPath); + fail("Calling Authorizable#getPropertyNames with " + invalidRelPath + " must fail."); + } catch (Exception e) { + // success + } + } + } + + @Test + public void testGetNotExistingProperty() throws RepositoryException, NotExecutableException { + Authorizable auth = getTestUser(superuser); + String hint = "Fullname"; + String propName = hint; + int i = 0; + while (auth.hasProperty(propName)) { + propName = hint + i; + i++; + } + assertNull(auth.getProperty(propName)); + assertFalse(auth.hasProperty(propName)); + } + + @Test + public void testRemoveNotExistingProperty() throws RepositoryException, NotExecutableException { + Authorizable auth = getTestUser(superuser); + String hint = "Fullname"; + String propName = hint; + int i = 0; + while (auth.hasProperty(propName)) { + propName = hint + i; + i++; + } + assertFalse(auth.removeProperty(propName)); + superuser.save(); + } + + /** + * Removing an authorizable that is still listed as member of a group. + * @throws javax.jcr.RepositoryException + * @throws org.apache.jackrabbit.test.NotExecutableException + */ + public void testRemoveListedAuthorizable() throws RepositoryException, NotExecutableException { + String newUserId = null; + Group newGroup = null; + + try { + Principal uP = getTestPrincipal(); + User newUser = userMgr.createUser(uP.getName(), uP.getName()); + superuser.save(); + newUserId = newUser.getID(); + + newGroup = userMgr.createGroup(getTestPrincipal()); + newGroup.addMember(newUser); + superuser.save(); + + // remove the new user that is still listed as member. + newUser.remove(); + superuser.save(); + } finally { + if (newUserId != null) { + Authorizable u = userMgr.getAuthorizable(newUserId); + if (u != null) { + if (newGroup != null) { + newGroup.removeMember(u); + } + u.remove(); + } + } + if (newGroup != null) { + newGroup.remove(); + } + superuser.save(); + } + } + + @Test + public void testSetSpecialProperties() throws NotExecutableException, RepositoryException { + Value v = superuser.getValueFactory().createValue("any_value"); + + for (String pName : protectedUserProps.keySet()) { + try { + boolean isMultiValued = protectedUserProps.get(pName); + if (isMultiValued) { + user.setProperty(pName, new Value[] {v}); + } else { + user.setProperty(pName, v); + } + superuser.save(); + fail("changing the '" + pName + "' property on a User should fail."); + } catch (RepositoryException e) { + // success + } finally { + superuser.refresh(false); + } + } + + for (String pName : protectedGroupProps.keySet()) { + try { + boolean isMultiValued = protectedGroupProps.get(pName); + if (isMultiValued) { + group.setProperty(pName, new Value[] {v}); + } else { + group.setProperty(pName, v); + } + superuser.save(); + fail("changing the '" + pName + "' property on a Group should fail."); + } catch (RepositoryException e) { + // success + } finally { + superuser.refresh(false); + } + } + } + + @Test + public void testRemoveSpecialProperties() throws NotExecutableException, RepositoryException { + for (String pName : protectedUserProps.keySet()) { + try { + if (user.removeProperty(pName)) { + superuser.save(); + fail("removing the '" + pName + "' property on a User should fail."); + } // else: property not present: fine as well. + } catch (RepositoryException e) { + // success + } finally { + superuser.refresh(false); + } + } + for (String pName : protectedGroupProps.keySet()) { + try { + if (group.removeProperty(pName)) { + superuser.save(); + fail("removing the '" + pName + "' property on a Group should fail."); + } // else: property not present. fine as well. + } catch (RepositoryException e) { + // success + } finally { + superuser.refresh(false); + } + } + } + + @Test + public void testProtectedUserProperties() throws NotExecutableException, RepositoryException { + User user = getTestUser(superuser); + Node n = getNode(user, superuser); + + if (n.hasProperty(UserConstants.REP_PASSWORD)) { + checkProtected(n.getProperty(UserConstants.REP_PASSWORD)); + } + if (n.hasProperty(UserConstants.REP_PRINCIPAL_NAME)) { + checkProtected(n.getProperty(UserConstants.REP_PRINCIPAL_NAME)); + } + if (n.hasProperty(UserConstants.REP_IMPERSONATORS)) { + checkProtected(n.getProperty(UserConstants.REP_IMPERSONATORS)); + } + } + + @Test + public void testProtectedGroupProperties() throws NotExecutableException, RepositoryException { + Node n = getNode(group, superuser); + + if (n.hasProperty(UserConstants.REP_PRINCIPAL_NAME)) { + checkProtected(n.getProperty(UserConstants.REP_PRINCIPAL_NAME)); + } + if (n.hasProperty(UserConstants.REP_MEMBERS)) { + checkProtected(n.getProperty(UserConstants.REP_MEMBERS)); + } + } + + @Test + public void testMembersPropertyType() throws NotExecutableException, RepositoryException { + Node n = getNode(group, superuser); + + if (!n.hasProperty(UserConstants.REP_MEMBERS)) { + group.addMember(getTestUser(superuser)); + } + + Property p = n.getProperty(UserConstants.REP_MEMBERS); + for (Value v : p.getValues()) { + assertEquals(PropertyType.WEAKREFERENCE, v.getType()); + } + } + + @Test + public void testSetSpecialPropertiesDirectly() throws NotExecutableException, RepositoryException { + Authorizable user = getTestUser(superuser); + Node n = getNode(user, superuser); + try { + String pName = user.getPrincipal().getName(); + n.setProperty(UserConstants.REP_PRINCIPAL_NAME, new StringValue("any-value")); + + // should have failed => change value back. + n.setProperty(UserConstants.REP_PRINCIPAL_NAME, new StringValue(pName)); + fail("Attempt to change protected property rep:principalName should fail."); + } catch (ConstraintViolationException e) { + // ok. + } + + try { + String imperson = "anyimpersonator"; + n.setProperty( + UserConstants.REP_IMPERSONATORS, + new Value[] {new StringValue(imperson)}, + PropertyType.STRING); + fail("Attempt to change protected property rep:impersonators should fail."); + } catch (ConstraintViolationException e) { + // ok. + } + } + + @Test + public void testRemoveSpecialUserPropertiesDirectly() throws RepositoryException, NotExecutableException { + Authorizable g = getTestUser(superuser); + Node n = getNode(g, superuser); + try { + n.getProperty(UserConstants.REP_PASSWORD).remove(); + fail("Attempt to remove protected property rep:password should fail."); + } catch (ConstraintViolationException e) { + // ok. + } + try { + if (n.hasProperty(UserConstants.REP_PRINCIPAL_NAME)) { + n.getProperty(UserConstants.REP_PRINCIPAL_NAME).remove(); + fail("Attempt to remove protected property rep:principalName should fail."); + } + } catch (ConstraintViolationException e) { + // ok. + } + } + + @Test + public void testRemoveSpecialGroupPropertiesDirectly() throws RepositoryException, NotExecutableException { + Node n = getNode(group, superuser); + try { + if (n.hasProperty(UserConstants.REP_PRINCIPAL_NAME)) { + n.getProperty(UserConstants.REP_PRINCIPAL_NAME).remove(); + fail("Attempt to remove protected property rep:principalName should fail."); + } + } catch (ConstraintViolationException e) { + // ok. + } + try { + if (n.hasProperty(UserConstants.REP_MEMBERS)) { + n.getProperty(UserConstants.REP_MEMBERS).remove(); + fail("Attempt to remove protected property rep:members should fail."); + } + } catch (ConstraintViolationException e) { + // ok. + } + } + + @Test + public void testUserGetProperties() throws RepositoryException, NotExecutableException { + Authorizable user = getTestUser(superuser); + Node n = getNode(user, superuser); + + for (PropertyIterator it = n.getProperties(); it.hasNext();) { + Property p = it.nextProperty(); + if (p.getDefinition().isProtected()) { + assertFalse(user.hasProperty(p.getName())); + assertNull(user.getProperty(p.getName())); + } else { + // authorizable defined property + assertTrue(user.hasProperty(p.getName())); + assertNotNull(user.getProperty(p.getName())); + } + } + } + + @Test + public void testGroupGetProperties() throws RepositoryException, NotExecutableException { + Node n = getNode(group, superuser); + + for (PropertyIterator it = n.getProperties(); it.hasNext();) { + Property prop = it.nextProperty(); + if (prop.getDefinition().isProtected()) { + assertFalse(group.hasProperty(prop.getName())); + assertNull(group.getProperty(prop.getName())); + } else { + // authorizable defined property + assertTrue(group.hasProperty(prop.getName())); + assertNotNull(group.getProperty(prop.getName())); + } + } + } + + @Test + public void testSingleToMultiValued() throws Exception { + Authorizable user = getTestUser(superuser); + UserManager uMgr = getUserManager(superuser); + try { + Value v = superuser.getValueFactory().createValue("anyValue"); + user.setProperty("someProp", v); + if (!uMgr.isAutoSave()) { + superuser.save(); + } + Value[] vs = new Value[] {v, v}; + user.setProperty("someProp", vs); + if (!uMgr.isAutoSave()) { + superuser.save(); + } + } finally { + if (user.removeProperty("someProp") && !uMgr.isAutoSave()) { + superuser.save(); + } + } + } + + @Test + public void testMultiValuedToSingle() throws Exception { + Authorizable user = getTestUser(superuser); + UserManager uMgr = getUserManager(superuser); + try { + Value v = superuser.getValueFactory().createValue("anyValue"); + Value[] vs = new Value[] {v, v}; + user.setProperty("someProp", vs); + if (!uMgr.isAutoSave()) { + superuser.save(); + } + user.setProperty("someProp", v); + if (!uMgr.isAutoSave()) { + superuser.save(); + } + } finally { + if (user.removeProperty("someProp") && !uMgr.isAutoSave()) { + superuser.save(); + } + } + } + + @Test + public void testObjectMethods() throws Exception { + final Authorizable user = getTestUser(superuser); + Authorizable user2 = getTestUser(superuser); + + assertEquals(user, user2); + assertEquals(user.hashCode(), user2.hashCode()); + Set s = new HashSet(); + s.add(user); + assertFalse(s.add(user2)); + + Authorizable user3 = new Authorizable() { + + public String getID() throws RepositoryException { + return user.getID(); + } + + public boolean isGroup() { + return user.isGroup(); + } + + public Principal getPrincipal() throws RepositoryException { + return user.getPrincipal(); + } + + public Iterator declaredMemberOf() throws RepositoryException { + return user.declaredMemberOf(); + } + + public Iterator memberOf() throws RepositoryException { + return user.memberOf(); + } + + public void remove() throws RepositoryException { + user.remove(); + } + + public Iterator getPropertyNames() throws RepositoryException { + return user.getPropertyNames(); + } + + public Iterator getPropertyNames(String relPath) throws RepositoryException { + return user.getPropertyNames(relPath); + } + + public boolean hasProperty(String name) throws RepositoryException { + return user.hasProperty(name); + } + + public void setProperty(String name, Value value) throws RepositoryException { + user.setProperty(name, value); + } + + public void setProperty(String name, Value[] values) throws RepositoryException { + user.setProperty(name, values); + } + + public Value[] getProperty(String name) throws RepositoryException { + return user.getProperty(name); + } + + public boolean removeProperty(String name) throws RepositoryException { + return user.removeProperty(name); + } + + public String getPath() throws UnsupportedRepositoryOperationException, RepositoryException { + return user.getPath(); + } + }; + + assertFalse(user.equals(user3)); + assertTrue(s.add(user3)); + } +} \ No newline at end of file diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/CreateGroupTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/CreateGroupTest.java new file mode 100644 index 00000000000..149bd24e7ed --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/CreateGroupTest.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.security.user; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.AuthorizableExistsException; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.test.NotExecutableException; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * CreateGroupTest... + */ +public class CreateGroupTest extends AbstractUserTest { + + private static Logger log = LoggerFactory.getLogger(CreateGroupTest.class); + + private List createdGroups = new ArrayList(); + + @Override + protected void tearDown() throws Exception { + // remove all created groups again + for (Authorizable createdGroup : createdGroups) { + try { + createdGroup.remove(); + superuser.save(); + } catch (RepositoryException e) { + log.error("Failed to remove Group " + createdGroup.getID() + " during tearDown."); + } + } + + super.tearDown(); + } + + private Group createGroup(Principal p) throws RepositoryException { + Group gr = userMgr.createGroup(p); + superuser.save(); + return gr; + } + + private Group createGroup(Principal p, String iPath) throws RepositoryException { + Group gr = userMgr.createGroup(p, iPath); + superuser.save(); + return gr; + } + + @Test + public void testCreateGroup() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + Group gr = createGroup(p); + createdGroups.add(gr); + + assertNotNull(gr.getID()); + assertEquals(p.getName(), gr.getPrincipal().getName()); + assertFalse("A new group must not have members.",gr.getMembers().hasNext()); + } + + // TODO: check again. +// @Test +// public void testCreateGroupWithPath() throws RepositoryException, NotExecutableException { +// Principal p = getTestPrincipal(); +// Group gr = createGroup(p, "/any/path/to/the/new/group"); +// createdGroups.add(gr); +// +// assertNotNull(gr.getID()); +// assertEquals(p.getName(), gr.getPrincipal().getName()); +// assertFalse("A new group must not have members.",gr.getMembers().hasNext()); +// } + + @Test + public void testCreateGroupWithNullPrincipal() throws RepositoryException { + try { + Group gr = createGroup(null); + createdGroups.add(gr); + + fail("A Group cannot be built from 'null' Principal"); + } catch (Exception e) { + // ok + } + + try { + Group gr = createGroup(null, "/any/path/to/the/new/group"); + createdGroups.add(gr); + + fail("A Group cannot be built from 'null' Principal"); + } catch (Exception e) { + // ok + } + } + + @Test + public void testCreateDuplicateGroup() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + Group gr = createGroup(p); + createdGroups.add(gr); + + try { + Group gr2 = createGroup(p); + createdGroups.add(gr2); + fail("Creating 2 groups with the same Principal should throw AuthorizableExistsException."); + } catch (AuthorizableExistsException e) { + // success. + } + } +} \ No newline at end of file diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/CreateUserTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/CreateUserTest.java new file mode 100644 index 00000000000..cf70ebddfbe --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/CreateUserTest.java @@ -0,0 +1,256 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.security.user; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.AuthorizableExistsException; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.test.NotExecutableException; +import org.junit.After; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * CreateUserTest... + */ +public class CreateUserTest extends AbstractUserTest { + + private static Logger log = LoggerFactory.getLogger(CreateUserTest.class); + + private List createdUsers = new ArrayList(); + + @After + @Override + protected void tearDown() throws Exception { + superuser.refresh(false); + // remove all created groups again + for (Object createdUser : createdUsers) { + Authorizable auth = (Authorizable) createdUser; + try { + auth.remove(); + superuser.save(); + } catch (RepositoryException e) { + log.warn("Failed to remove User " + auth.getID() + " during tearDown."); + } + } + super.tearDown(); + } + + private User createUser(String uid, String pw) throws RepositoryException, NotExecutableException { + User u = userMgr.createUser(uid, pw); + superuser.save(); + return u; + } + + private User createUser(String uid, String pw, Principal p, String iPath) throws RepositoryException, NotExecutableException { + User u = userMgr.createUser(uid, pw, p, iPath); + superuser.save(); + return u; + } + + @Test + public void testCreateUser() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = p.getName(); + User user = createUser(uid, "pw"); + createdUsers.add(user); + + assertNotNull(user.getID()); + assertEquals(p.getName(), user.getPrincipal().getName()); + } + + // TODO: check again. +// @Test +// public void testCreateUserWithPath() throws RepositoryException, NotExecutableException { +// Principal p = getTestPrincipal(); +// String uid = p.getName(); +// User user = createUser(uid, "pw", p, "/any/path/to/the/new/user"); +// createdUsers.add(user); +// +// assertNotNull(user.getID()); +// assertEquals(p.getName(), user.getPrincipal().getName()); +// } + + @Test + public void testCreateUserWithPath2() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = p.getName(); + User user = createUser(uid, "pw", p, "any/path"); + createdUsers.add(user); + + assertNotNull(user.getID()); + assertEquals(p.getName(), user.getPrincipal().getName()); + } + + @Test + public void testCreateUserWithDifferentPrincipalName() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = getTestPrincipal().getName(); + User user = createUser(uid, "pw", p, "any/path"); + createdUsers.add(user); + + assertNotNull(user.getID()); + assertEquals(p.getName(), user.getPrincipal().getName()); + } + + @Test + public void testCreateUserWithNullParamerters() throws RepositoryException { + try { + User user = createUser(null, null); + createdUsers.add(user); + + fail("A User cannot be built from 'null' parameters"); + } catch (Exception e) { + // ok + } + + try { + User user = createUser(null, null, null, null); + createdUsers.add(user); + + fail("A User cannot be built from 'null' parameters"); + } catch (Exception e) { + // ok + } + } + + @Test + public void testCreateUserWithNullUserID() throws RepositoryException { + try { + User user = createUser(null, "anyPW"); + createdUsers.add(user); + + fail("A User cannot be built with 'null' userID"); + } catch (Exception e) { + // ok + } + } + + @Test + public void testCreateUserWithEmptyUserID() throws RepositoryException { + try { + User user = createUser("", "anyPW"); + createdUsers.add(user); + + fail("A User cannot be built with \"\" userID"); + } catch (Exception e) { + // ok + } + try { + User user = createUser("", "anyPW", getTestPrincipal(), null); + createdUsers.add(user); + + fail("A User cannot be built with \"\" userID"); + } catch (Exception e) { + // ok + } + } + + @Test + public void testCreateUserWithEmptyPassword() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + User user = createUser(p.getName(), ""); + createdUsers.add(user); + } + + @Test + public void testCreateUserWithNullPrincipal() throws RepositoryException { + try { + Principal p = getTestPrincipal(); + String uid = p.getName(); + User user = createUser(uid, "pw", null, "/a/b/c"); + createdUsers.add(user); + + fail("A User cannot be built with 'null' Principal"); + } catch (Exception e) { + // ok + } + } + + public void testCreateUserWithEmptyPrincipal() throws RepositoryException { + try { + Principal p = getTestPrincipal(""); + String uid = p.getName(); + User user = createUser(uid, "pw", p, "/a/b/c"); + createdUsers.add(user); + + fail("A User cannot be built with ''-named Principal"); + } catch (Exception e) { + // ok + } + try { + Principal p = getTestPrincipal(null); + String uid = p.getName(); + User user = createUser(uid, "pw", p, "/a/b/c"); + createdUsers.add(user); + + fail("A User cannot be built with ''-named Principal"); + } catch (Exception e) { + // ok + } + } + + public void testCreateTwiceWithSameUserID() throws RepositoryException, NotExecutableException { + String uid = getTestPrincipal().getName(); + User user = createUser(uid, "pw"); + createdUsers.add(user); + + try { + User user2 = createUser(uid, "anyPW"); + createdUsers.add(user2); + + fail("Creating 2 users with the same UserID should throw AuthorizableExistsException."); + } catch (AuthorizableExistsException e) { + // success. + } + } + + // TODO: RepositoryException is thrown instead of AuthorizableExistsException + public void testCreateTwiceWithSamePrincipal() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = p.getName(); + User user = createUser(uid, "pw", p, "a/b/c"); + createdUsers.add(user); + + try { + uid = getTestPrincipal().getName(); + User user2 = createUser(uid, "pw", p, null); + createdUsers.add(user2); + + fail("Creating 2 users with the same Principal should throw AuthorizableExistsException."); + } catch (RepositoryException e) { + // success. + } + } + + public void testGetUserAfterCreation() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = p.getName(); + + User user = createUser(uid, "pw"); + createdUsers.add(user); + + assertNotNull(userMgr.getAuthorizable(user.getID())); + assertNotNull(userMgr.getAuthorizable(p)); + } +} \ No newline at end of file diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/EveryoneGroupTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/EveryoneGroupTest.java new file mode 100644 index 00000000000..fdb5af3c3f1 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/EveryoneGroupTest.java @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.security.user; + +import java.security.Principal; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.test.NotExecutableException; +import org.junit.Test; + +/** + * Tests for the group associated with {@code EveryonePrincipal} + */ +public class EveryoneGroupTest extends AbstractUserTest { + + private Group everyone; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + everyone = userMgr.createGroup(EveryonePrincipal.NAME); + superuser.save(); + } + + @Override + protected void tearDown() throws Exception { + try { + everyone.remove(); + superuser.save(); + } finally { + super.tearDown(); + } + } + + @Test + public void testEveryoneGroup() throws RepositoryException, NotExecutableException { + assertEquals(EveryonePrincipal.NAME, everyone.getPrincipal().getName()); + } + + @Test + public void testPrincipal() throws RepositoryException { + assertEquals(EveryonePrincipal.getInstance(), everyone.getPrincipal()); + } + + @Test + public void testGroupPrincipal() throws Exception { + Principal everyonePrincipal = everyone.getPrincipal(); + assertTrue(everyonePrincipal instanceof java.security.acl.Group); + assertTrue(everyonePrincipal.equals(EveryonePrincipal.getInstance())); + assertTrue(EveryonePrincipal.getInstance().equals(everyonePrincipal)); + + java.security.acl.Group gr = (java.security.acl.Group) everyonePrincipal; + assertFalse(gr.isMember(everyonePrincipal)); + assertTrue(gr.isMember(getTestUser(superuser).getPrincipal())); + assertTrue(gr.isMember(new Principal() { + public String getName() { + return "test"; + } + })); + } + + @Test + public void testMembers() throws RepositoryException, NotExecutableException { + assertTrue(everyone.isDeclaredMember(getTestUser(superuser))); + assertTrue(everyone.isMember(getTestUser(superuser))); + + Iterator it = everyone.getDeclaredMembers(); + assertTrue(it.hasNext()); + Set members = new HashSet(); + while (it.hasNext()) { + members.add(it.next()); + } + + it = everyone.getMembers(); + assertTrue(it.hasNext()); + while (it.hasNext()) { + assertTrue(members.contains(it.next())); + } + } + + @Test + public void testEditMembers() throws RepositoryException, NotExecutableException { + assertFalse(everyone.addMember(getTestUser(superuser))); + assertFalse(everyone.removeMember(getTestUser(superuser))); + + Group anotherGroup = null; + try { + anotherGroup = userMgr.createGroup("testGroup"); + superuser.save(); + + assertFalse(everyone.addMember(anotherGroup)); + assertFalse(everyone.removeMember(anotherGroup)); + + assertFalse(anotherGroup.addMember(everyone)); + assertFalse(anotherGroup.removeMember(everyone)); + + } finally { + if (anotherGroup != null) { + anotherGroup.remove(); + superuser.save(); + } + } + } +} \ No newline at end of file diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupTest.java new file mode 100644 index 00000000000..c16d4408297 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupTest.java @@ -0,0 +1,695 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.security.user; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.AuthorizableExistsException; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.test.NotExecutableException; +import org.junit.Before; +import org.junit.Test; + +/** + * GroupTest... + */ +public class GroupTest extends AbstractUserTest { + + private List members = new ArrayList(); + + @Before + @Override + protected void setUp() throws Exception { + super.setUp(); + + group.addMember(userMgr.getAuthorizable(superuser.getUserID())); + group.addMember(user); + + members.add(superuser.getUserID()); + members.add(user.getID()); + + superuser.save(); + } + + private static void assertTrueIsMember(Iterator members, Authorizable auth) throws RepositoryException { + boolean contained = false; + while (members.hasNext() && !contained) { + Object next = members.next(); + assertTrue(next instanceof Authorizable); + contained = ((Authorizable)next).getID().equals(auth.getID()); + } + assertTrue("The given set of members must contain '" + auth.getID() + '\'', contained); + } + + private static void assertFalseIsMember(Iterator members, Authorizable auth) throws RepositoryException { + boolean contained = false; + while (members.hasNext() && !contained) { + Object next = members.next(); + assertTrue(next instanceof Authorizable); + contained = ((Authorizable)next).getID().equals(auth.getID()); + } + assertFalse("The given set of members must not contain '" + auth.getID() + '\'', contained); + } + + private static void assertTrueMemberOfContainsGroup(Iterator groups, Group gr) throws RepositoryException { + boolean contained = false; + while (groups.hasNext() && !contained) { + Object next = groups.next(); + assertTrue(next instanceof Group); + contained = ((Group)next).getID().equals(gr.getID()); + } + assertTrue("All members of a group must contain that group upon 'memberOf'.", contained); + } + + private static void assertFalseMemberOfContainsGroup(Iterator groups, Group gr) throws RepositoryException { + boolean contained = false; + while (groups.hasNext() && !contained) { + Object next = groups.next(); + assertTrue(next instanceof Group); + contained = ((Group)next).getID().equals(gr.getID()); + } + assertFalse("All members of a group must contain that group upon 'memberOf'.", contained); + } + + @Test + public void testIsGroup() throws NotExecutableException, RepositoryException { + assertTrue(group.isGroup()); + } + + @Test + public void testGetID() throws NotExecutableException, RepositoryException { + assertNotNull(group.getID()); + assertNotNull(userMgr.getAuthorizable(group.getID()).getID()); + } + + @Test + public void testGetPrincipal() throws RepositoryException, NotExecutableException { + assertNotNull(group.getPrincipal()); + assertNotNull(userMgr.getAuthorizable(group.getID()).getPrincipal()); + } + + @Test + public void testGetPath() throws RepositoryException, NotExecutableException { + assertNotNull(group.getPath()); + assertNotNull(userMgr.getAuthorizable(group.getID()).getPath()); + try { + assertEquals(getNode(group, superuser).getPath(), group.getPath()); + } catch (UnsupportedRepositoryOperationException e) { + // ok. + } + } + + @Test + public void testGetDeclaredMembers() throws NotExecutableException, RepositoryException { + Iterator it = group.getDeclaredMembers(); + assertNotNull(it); + while (it.hasNext()) { + Authorizable a = it.next(); + assertNotNull(a); + members.remove(a.getID()); + } + assertTrue(members.isEmpty()); + } + + @Test + public void testGetMembers() throws NotExecutableException, RepositoryException { + Iterator it = group.getMembers(); + assertNotNull(it); + while (it.hasNext()) { + assertTrue(it.next() != null); + } + } + + @Test + public void testGetMembersAgainstIsMember() throws NotExecutableException, RepositoryException { + Iterator it = group.getMembers(); + while (it.hasNext()) { + Authorizable auth = it.next(); + assertTrue(group.isMember(auth)); + } + } + + @Test + public void testGetMembersAgainstMemberOf() throws NotExecutableException, RepositoryException { + Iterator it = group.getMembers(); + while (it.hasNext()) { + Authorizable auth = it.next(); + assertTrueMemberOfContainsGroup(auth.memberOf(), group); + } + } + + @Test + public void testGetDeclaredMembersAgainstDeclaredMemberOf() throws NotExecutableException, RepositoryException { + Iterator it = group.getDeclaredMembers(); + while (it.hasNext()) { + Authorizable auth = it.next(); + assertTrueMemberOfContainsGroup(auth.declaredMemberOf(), group); + } + } + + @Test + public void testGetMembersContainsDeclaredMembers() throws NotExecutableException, RepositoryException { + List l = new ArrayList(); + for (Iterator it = group.getMembers(); it.hasNext();) { + l.add(it.next().getID()); + } + for (Iterator it = group.getDeclaredMembers(); it.hasNext();) { + assertTrue("All declared members must also be part of the Iterator " + + "returned upon getMembers()",l.contains(it.next().getID())); + } + } + + @Test + public void testAddMember() throws NotExecutableException, RepositoryException { + User auth = getTestUser(superuser); + Group newGroup = null; + try { + newGroup = userMgr.createGroup(createGroupId()); + superuser.save(); + + assertFalse(newGroup.isMember(auth)); + assertFalse(newGroup.removeMember(auth)); + superuser.save(); + + assertTrue(newGroup.addMember(auth)); + superuser.save(); + assertTrue(newGroup.isMember(auth)); + assertTrue(newGroup.isMember(userMgr.getAuthorizable(auth.getID()))); + + } finally { + if (newGroup != null) { + newGroup.removeMember(auth); + newGroup.remove(); + superuser.save(); + } + } + } + + @Test + public void testAddMembers() throws NotExecutableException, RepositoryException { + User auth = getTestUser(superuser); + Group newGroup = null; + int size = 100; + List users = new ArrayList(size); + try { + newGroup = userMgr.createGroup(createGroupId()); + superuser.save(); + + for (int k = 0; k < size; k++) { + users.add(userMgr.createUser("user_" + k, "pass_" + k)); + } + superuser.save(); + + for (User user : users) { + assertTrue(newGroup.addMember(user)); + } + superuser.save(); + + for (User user : users) { + assertTrue(newGroup.isMember(user)); + } + + for (User user : users) { + assertTrue(newGroup.removeMember(user)); + } + superuser.save(); + + for (User user : users) { + assertFalse(newGroup.isMember(user)); + } + } finally { + for (User user : users) { + user.remove(); + superuser.save(); + } + if (newGroup != null) { + newGroup.removeMember(auth); + newGroup.remove(); + superuser.save(); + } + } + } + + @Test + public void testAddRemoveMember() throws NotExecutableException, RepositoryException { + User auth = getTestUser(superuser); + Group newGroup1 = null; + Group newGroup2 = null; + try { + newGroup1 = userMgr.createGroup(createGroupId()); + newGroup2 = userMgr.createGroup(createGroupId()); + superuser.save(); + + assertFalse(newGroup1.isMember(auth)); + assertFalse(newGroup1.removeMember(auth)); + superuser.save(); + assertFalse(newGroup2.isMember(auth)); + assertFalse(newGroup2.removeMember(auth)); + superuser.save(); + + assertTrue(newGroup1.addMember(auth)); + superuser.save(); + assertTrue(newGroup1.isMember(auth)); + assertTrue(newGroup1.isMember(userMgr.getAuthorizable(auth.getID()))); + + assertTrue(newGroup2.addMember(auth)); + superuser.save(); + assertTrue(newGroup2.isMember(auth)); + assertTrue(newGroup2.isMember(userMgr.getAuthorizable(auth.getID()))); + + assertTrue(newGroup1.removeMember(auth)); + superuser.save(); + assertTrue(newGroup2.removeMember(auth)); + superuser.save(); + + assertTrue(newGroup1.addMember(auth)); + superuser.save(); + assertTrue(newGroup1.isMember(auth)); + assertTrue(newGroup1.isMember(userMgr.getAuthorizable(auth.getID()))); + assertTrue(newGroup1.removeMember(auth)); + superuser.save(); + + + } finally { + if (newGroup1 != null) { + newGroup1.removeMember(auth); + newGroup1.remove(); + superuser.save(); + } + if (newGroup2 != null) { + newGroup2.removeMember(auth); + newGroup2.remove(); + superuser.save(); + } + } + } + + @Test + public void testIsDeclaredMember() throws RepositoryException, NotExecutableException { + User auth = getTestUser(superuser); + Group newGroup1 = null; + Group newGroup2 = null; + try { + newGroup1 = userMgr.createGroup(createGroupId()); + newGroup2 = userMgr.createGroup(createGroupId()); + superuser.save(); + + assertFalse(newGroup1.isDeclaredMember(auth)); + assertFalse(newGroup2.isDeclaredMember(auth)); + + assertTrue(newGroup2.addMember(auth)); + superuser.save(); + assertTrue(newGroup2.isDeclaredMember(auth)); + assertTrue(newGroup2.isDeclaredMember(userMgr.getAuthorizable(auth.getID()))); + + assertTrue(newGroup1.addMember(newGroup2)); + superuser.save(); + assertTrue(newGroup1.isDeclaredMember(newGroup2)); + assertTrue(newGroup1.isDeclaredMember(userMgr.getAuthorizable(newGroup2.getID()))); + assertTrue(newGroup1.isMember(auth)); + assertTrue(newGroup1.isMember(userMgr.getAuthorizable(auth.getID()))); + assertFalse(newGroup1.isDeclaredMember(auth)); + assertFalse(newGroup1.isDeclaredMember(userMgr.getAuthorizable(auth.getID()))); + } finally { + if (newGroup1 != null) { + newGroup1.remove(); + superuser.save(); + } + if (newGroup2 != null) { + newGroup2.remove(); + superuser.save(); + } + } + } + + @Test + public void testAddMemberTwice() throws NotExecutableException, RepositoryException { + User auth = getTestUser(superuser); + Group newGroup = null; + try { + newGroup = userMgr.createGroup(createGroupId()); + superuser.save(); + + assertTrue(newGroup.addMember(auth)); + superuser.save(); + assertFalse(newGroup.addMember(auth)); + superuser.save(); + assertTrue(newGroup.isMember(auth)); + + } finally { + if (newGroup != null) { + newGroup.removeMember(auth); + newGroup.remove(); + superuser.save(); + } + } + } + + @Test + public void testAddMemberModifiesMemberOf() throws NotExecutableException, RepositoryException { + User auth = getTestUser(superuser); + Group newGroup = null; + try { + newGroup = userMgr.createGroup(createGroupId()); + superuser.save(); + + assertFalseMemberOfContainsGroup(auth.memberOf(), newGroup); + assertTrue(newGroup.addMember(auth)); + superuser.save(); + + assertTrueMemberOfContainsGroup(auth.declaredMemberOf(), newGroup); + assertTrueMemberOfContainsGroup(auth.memberOf(), newGroup); + } finally { + if (newGroup != null) { + newGroup.removeMember(auth); + newGroup.remove(); + superuser.save(); + } + } + } + + @Test + public void testAddMemberModifiesGetMembers() throws NotExecutableException, RepositoryException { + User auth = getTestUser(superuser); + Group newGroup = null; + try { + newGroup = userMgr.createGroup(createGroupId()); + superuser.save(); + + assertFalseIsMember(newGroup.getMembers(), auth); + assertFalseIsMember(newGroup.getDeclaredMembers(), auth); + assertTrue(newGroup.addMember(auth)); + superuser.save(); + + assertTrueIsMember(newGroup.getMembers(), auth); + assertTrueIsMember(newGroup.getDeclaredMembers(), auth); + } finally { + if (newGroup != null) { + newGroup.removeMember(auth); + newGroup.remove(); + superuser.save(); + } + } + } + + @Test + public void testIndirectMembers() throws NotExecutableException, RepositoryException { + User user = getTestUser(superuser); + Group newGroup = null; + Group newGroup2 = null; + try { + newGroup = userMgr.createGroup(createGroupId()); + newGroup2 = userMgr.createGroup(createGroupId()); + superuser.save(); + + newGroup.addMember(newGroup2); + superuser.save(); + assertTrue(newGroup.isMember(newGroup2)); + + newGroup2.addMember(user); + superuser.save(); + + // testuser must not be declared member of 'newGroup' + assertFalseIsMember(newGroup.getDeclaredMembers(), user); + assertFalseMemberOfContainsGroup(user.declaredMemberOf(), newGroup); + + // testuser must however be member of 'newGroup' (indirect). + assertTrueIsMember(newGroup.getMembers(), user); + assertTrueMemberOfContainsGroup(user.memberOf(), newGroup); + + // testuser cannot be removed from 'newGroup' + assertFalse(newGroup.removeMember(user)); + superuser.save(); + } finally { + if (newGroup != null) { + newGroup.removeMember(newGroup2); + newGroup.remove(); + superuser.save(); + } + if (newGroup2 != null) { + newGroup2.removeMember(user); + newGroup2.remove(); + superuser.save(); + } + } + } + + @Test + public void testMembersInPrincipal() throws NotExecutableException, RepositoryException { + User auth = getTestUser(superuser); + Group newGroup = null; + Group newGroup2 = null; + try { + newGroup = userMgr.createGroup(createGroupId()); + newGroup2 = userMgr.createGroup(createGroupId()); + superuser.save(); + + newGroup.addMember(newGroup2); + superuser.save(); + newGroup2.addMember(auth); + superuser.save(); + + java.security.acl.Group ngPrincipal = (java.security.acl.Group) newGroup.getPrincipal(); + java.security.acl.Group ng2Principal = (java.security.acl.Group) newGroup2.getPrincipal(); + + assertFalse(ng2Principal.isMember(ngPrincipal)); + + // newGroup2 must be member of newGroup's principal + assertTrue(ngPrincipal.isMember(newGroup2.getPrincipal())); + + // testuser must be member of newGroup2's and newGroup's principal (indirect) + assertTrue(ng2Principal.isMember(auth.getPrincipal())); + assertTrue(ngPrincipal.isMember(auth.getPrincipal())); + + } finally { + if (newGroup != null) { + newGroup.removeMember(newGroup2); + newGroup.remove(); + superuser.save(); + } + if (newGroup2 != null) { + newGroup2.removeMember(auth); + newGroup2.remove(); + superuser.save(); + } + } + } + + @Test + public void testDeeplyNestedGroups() throws NotExecutableException, RepositoryException { + Set groups = new HashSet(); + try { + User auth = getTestUser(superuser); + Group topGroup = userMgr.createGroup(createGroupId()); + + // Create chain of nested groups with auth member of bottom group + Group bottomGroup = topGroup; + for (int k = 0; k < 100; k++) { + Group g = userMgr.createGroup(createGroupId()); + groups.add(g); + bottomGroup.addMember(g); + bottomGroup = g; + } + bottomGroup.addMember(auth); + + // Check that every groups has exactly one member + for (Group g : groups) { + Iterator declaredMembers = g.getDeclaredMembers(); + assertTrue(declaredMembers.hasNext()); + declaredMembers.next(); + assertFalse(declaredMembers.hasNext()); + } + + // Check that we get all members from the getMembers call + HashSet allGroups = new HashSet(groups); + for (Iterator it = topGroup.getMembers(); it.hasNext(); ) { + Authorizable a = it.next(); + assertTrue(a.equals(auth) || allGroups.remove(a)); + } + assertTrue(allGroups.isEmpty()); + } finally { + for (Group g : groups) { + g.remove(); + } + } + } + + @Test + public void testInheritedMembers() throws Exception { + Set authorizables = new HashSet(); + try { + User testUser = userMgr.createUser(createUserId(), "pw"); + authorizables.add(testUser); + Group group1 = userMgr.createGroup(createGroupId()); + authorizables.add(group1); + Group group2 = userMgr.createGroup(createGroupId()); + authorizables.add(group2); + Group group3 = userMgr.createGroup(createGroupId()); + + group1.addMember(testUser); + group2.addMember(testUser); + group3.addMember(group1); + group3.addMember(group2); + + Iterator members = group3.getMembers(); + while (members.hasNext()) { + Authorizable a = members.next(); + assertTrue(authorizables.contains(a)); + assertTrue(authorizables.remove(a)); + } + + assertTrue(authorizables.isEmpty()); + } finally { + for (Authorizable a : authorizables) { + a.remove(); + } + } + } + + @Test + public void testCyclicGroups() throws AuthorizableExistsException, RepositoryException, NotExecutableException { + Group group1 = null; + Group group2 = null; + Group group3 = null; + try { + group1 = userMgr.createGroup(createGroupId()); + group2 = userMgr.createGroup(createGroupId()); + group3 = userMgr.createGroup(createGroupId()); + + group1.addMember(getTestUser(superuser)); + group2.addMember(getTestUser(superuser)); + + assertTrue(group1.addMember(group2)); + assertTrue(group2.addMember(group3)); + assertFalse(group3.addMember(group1)); + } finally { + if (group1 != null) group1.remove(); + if (group2 != null) group2.remove(); + if (group3 != null) group3.remove(); + } + } + + @Test + public void testRemoveMemberTwice() throws NotExecutableException, RepositoryException { + User auth = getTestUser(superuser); + Group newGroup = null; + try { + newGroup = userMgr.createGroup(createGroupId()); + superuser.save(); + + assertTrue(newGroup.addMember(auth)); + superuser.save(); + assertTrue(newGroup.removeMember(userMgr.getAuthorizable(auth.getID()))); + superuser.save(); + assertFalse(newGroup.removeMember(auth)); + superuser.save(); + } finally { + if (newGroup != null) { + newGroup.remove(); + superuser.save(); + } + } + } + + @Test + public void testAddItselfAsMember() throws RepositoryException, NotExecutableException { + Group newGroup = null; + try { + newGroup = userMgr.createGroup(createGroupId()); + superuser.save(); + + assertFalse(newGroup.addMember(newGroup)); + superuser.save(); + newGroup.removeMember(newGroup); + superuser.save(); + } finally { + if (newGroup != null) { + newGroup.remove(); + superuser.save(); + } + } + } + + @Test + public void testRemoveGroupIfMemberExist() throws RepositoryException, NotExecutableException { + User auth = getTestUser(superuser); + String newGroupId = null; + + try { + Group newGroup = userMgr.createGroup(createGroupId()); + superuser.save(); + newGroupId = newGroup.getID(); + + assertTrue(newGroup.addMember(auth)); + newGroup.remove(); + superuser.save(); + } finally { + Group gr = (Group) userMgr.getAuthorizable(newGroupId); + if (gr != null) { + gr.removeMember(auth); + gr.remove(); + superuser.save(); + } + } + } + + @Test + public void testRemoveGroupClearsMembership() throws NotExecutableException, RepositoryException { + User auth = getTestUser(superuser); + Group newGroup = null; + String groupId; + try { + newGroup = userMgr.createGroup(createGroupId()); + groupId = newGroup.getID(); + superuser.save(); + + assertTrue(newGroup.addMember(auth)); + superuser.save(); + + boolean isMember = false; + Iterator it = auth.declaredMemberOf(); + while (it.hasNext() && !isMember) { + isMember = groupId.equals(it.next().getID()); + } + assertTrue(isMember); + + } finally { + if (newGroup != null) { + newGroup.remove(); + superuser.save(); + } + } + + Iterator it = auth.declaredMemberOf(); + while (it.hasNext()) { + assertFalse(groupId.equals(it.next().getID())); + } + + it = auth.memberOf(); + while (it.hasNext()) { + assertFalse(groupId.equals(it.next().getID())); + } + } +} \ No newline at end of file diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/ImpersonationTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/ImpersonationTest.java new file mode 100644 index 00000000000..ddd62cbec80 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/ImpersonationTest.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.security.user; + +import java.security.Principal; +import java.util.Collections; +import javax.jcr.RepositoryException; +import javax.security.auth.Subject; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Impersonation; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.oak.spi.security.principal.AdminPrincipal; +import org.apache.jackrabbit.test.NotExecutableException; +import org.junit.Test; + +/** + * ImpersonationTest... + */ +public class ImpersonationTest extends AbstractUserTest { + + private User user2; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + user2 = userMgr.createUser("user2", "pw"); + superuser.save(); + } + + @Override + protected void tearDown() throws Exception { + try { + user2.remove(); + superuser.save(); + } finally { + super.tearDown(); + } + } + + @Test + public void testImpersonation() throws RepositoryException, NotExecutableException { + Principal user2Principal = user2.getPrincipal(); + Subject subject = new Subject(true, Collections.singleton(user2Principal), Collections.emptySet(), Collections.emptySet()); + + Impersonation impers = user.getImpersonation(); + assertFalse(impers.allows(subject)); + + assertTrue(impers.grantImpersonation(user2Principal)); + assertFalse(impers.grantImpersonation(user2Principal)); + superuser.save(); + + assertTrue(impers.allows(subject)); + + assertTrue(impers.revokeImpersonation(user2Principal)); + assertFalse(impers.revokeImpersonation(user2Principal)); + superuser.save(); + + assertFalse(impers.allows(subject)); + } + + @Test + public void testAdminAsImpersonator() throws RepositoryException, NotExecutableException { + String adminId = superuser.getUserID(); + Authorizable admin = userMgr.getAuthorizable(adminId); + if (admin == null || admin.isGroup() || !((User) admin).isAdmin()) { + throw new NotExecutableException(adminId + " is not administators ID"); + } + + Principal adminPrincipal = admin.getPrincipal(); + + // admin cannot be add/remove to set of impersonators of 'u' but is + // always allowed to impersonate that user. + Impersonation impersonation = user.getImpersonation(); + + assertFalse(impersonation.grantImpersonation(adminPrincipal)); + assertFalse(impersonation.revokeImpersonation(adminPrincipal)); + assertTrue(impersonation.allows(buildSubject(adminPrincipal))); + + // same if the impersonation object of the admin itself is used. + Impersonation adminImpersonation = ((User) admin).getImpersonation(); + + assertFalse(adminImpersonation.grantImpersonation(adminPrincipal)); + assertFalse(adminImpersonation.revokeImpersonation(adminPrincipal)); + assertTrue(impersonation.allows(buildSubject(adminPrincipal))); + } + + public void testAdminPrincipalAsImpersonator() throws RepositoryException, NotExecutableException { + + Principal adminPrincipal = new AdminPrincipal() { + @Override + public String getName() { + return "some-admin-name"; + } + }; + + // admin cannot be add/remove to set of impersonators of 'u' but is + // always allowed to impersonate that user. + Impersonation impersonation = user.getImpersonation(); + + assertFalse(impersonation.grantImpersonation(adminPrincipal)); + assertFalse(impersonation.revokeImpersonation(adminPrincipal)); + assertTrue(impersonation.allows(buildSubject(adminPrincipal))); + } +} \ No newline at end of file diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/NestedGroupTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/NestedGroupTest.java new file mode 100644 index 00000000000..b3eb222d0e7 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/NestedGroupTest.java @@ -0,0 +1,188 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.security.user; + +import java.security.Principal; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.test.NotExecutableException; +import org.junit.Test; + +/** + * NestedGroupTest... + */ +public class NestedGroupTest extends AbstractUserTest { + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + private Group createGroup(Principal p) throws RepositoryException { + Group gr = userMgr.createGroup(p); + superuser.save(); + return gr; + } + + private void removeGroup(Group gr) throws RepositoryException { + gr.remove(); + superuser.save(); + } + + private boolean addMember(Group gr, Authorizable member) throws RepositoryException { + boolean added = gr.addMember(member); + superuser.save(); + return added; + } + + private boolean removeMember(Group gr, Authorizable member) throws RepositoryException { + boolean removed = gr.removeMember(member); + superuser.save(); + return removed; + } + + @Test + public void testAddGroupAsMember() throws NotExecutableException, RepositoryException { + Group gr1 = null; + Group gr2 = null; + + try { + gr1 = createGroup(getTestPrincipal()); + gr2 = createGroup(getTestPrincipal()); + + assertFalse(gr1.isMember(gr2)); + + assertTrue(addMember(gr1, gr2)); + assertTrue(gr1.isMember(gr2)); + + } finally { + if (gr1 != null) { + removeMember(gr1, gr2); + removeGroup(gr1); + } + if (gr2 != null) { + removeGroup(gr2); + } + } + } + + @Test + public void testAddCircularMembers() throws NotExecutableException, RepositoryException { + Group gr1 = null; + Group gr2 = null; + + try { + gr1 = createGroup(getTestPrincipal()); + gr2 = createGroup(getTestPrincipal()); + + assertTrue(addMember(gr1, gr2)); + assertFalse(addMember(gr2, gr1)); + + } finally { + if (gr1 != null && gr1.isMember(gr2)) { + removeMember(gr1, gr2); + } + if (gr2 != null && gr2.isMember(gr1)) { + removeMember(gr2, gr1); + } + if (gr1 != null) removeGroup(gr1); + if (gr2 != null) removeGroup(gr2); + } + } + + @Test + public void testCyclicMembers2() throws RepositoryException, NotExecutableException { + Group gr1 = null; + Group gr2 = null; + Group gr3 = null; + try { + gr1 = createGroup(getTestPrincipal()); + gr2 = createGroup(getTestPrincipal()); + gr3 = createGroup(getTestPrincipal()); + + assertTrue(addMember(gr1, gr2)); + assertTrue(addMember(gr2, gr3)); + assertFalse(addMember(gr3, gr1)); + + } finally { + if (gr1 != null) { + removeMember(gr1, gr2); + } + if (gr2 != null) { + removeMember(gr2, gr3); + removeGroup(gr2); + } + if (gr3 != null) { + removeMember(gr3, gr1); + removeGroup(gr3); + } + if (gr1 != null) removeGroup(gr1); + + } + } + + @Test + public void testInheritedMembership() throws NotExecutableException, RepositoryException { + Group gr1 = null; + Group gr2 = null; + Group gr3 = null; + + if (!(superuser instanceof JackrabbitSession)) { + throw new NotExecutableException(); + } + + try { + gr1 = createGroup(getTestPrincipal()); + gr2 = createGroup(getTestPrincipal()); + gr3 = createGroup(getTestPrincipal()); + + assertTrue(addMember(gr1, gr2)); + assertTrue(addMember(gr2, gr3)); + + // NOTE: don't test with Group.isMember for not required to detect + // inherited membership -> rather with PrincipalManager. + boolean isMember = false; + PrincipalManager pmgr = ((JackrabbitSession) superuser).getPrincipalManager(); + for (PrincipalIterator it = pmgr.getGroupMembership(gr3.getPrincipal()); + it.hasNext() && !isMember;) { + isMember = it.nextPrincipal().equals(gr1.getPrincipal()); + } + assertTrue(isMember); + + } finally { + if (gr1 != null && gr1.isMember(gr2)) { + removeMember(gr1, gr2); + } + if (gr2 != null && gr2.isMember(gr3)) { + removeMember(gr2, gr3); + } + if (gr1 != null) removeGroup(gr1); + if (gr2 != null) removeGroup(gr2); + if (gr3 != null) removeGroup(gr3); + } + } +} \ No newline at end of file diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserManagerTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserManagerTest.java new file mode 100644 index 00000000000..127488a9610 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserManagerTest.java @@ -0,0 +1,836 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.security.user; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import javax.jcr.Credentials; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.AuthorizableExistsException; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.apache.jackrabbit.test.NotExecutableException; +import org.junit.Test; + +public class UserManagerTest extends AbstractUserTest { + + @Test + public void testGetNewAuthorizable() throws RepositoryException, NotExecutableException { + String uid = createUserId(); + User user = null; + + try { + user = userMgr.createUser(uid, uid); + assertEquals(uid, user.getID()); + assertNotNull(userMgr.getAuthorizable(uid)); + assertEquals(user, userMgr.getAuthorizable(uid)); + + assertNotNull(getNode(user, superuser)); + } finally { + if (user != null) { + user.remove(); + } + } + } + + @Test + public void testGetAuthorizable() throws RepositoryException, NotExecutableException { + String uid = createUserId(); + User user = null; + + try { + user = userMgr.createUser(uid, uid); + superuser.save(); + + assertEquals(uid, user.getID()); + assertNotNull(userMgr.getAuthorizable(uid)); + assertEquals(user, userMgr.getAuthorizable(uid)); + + assertNotNull(getNode(user, superuser)); + } finally { + if (user != null) { + user.remove(); + } + } + } + + @Test + public void testGetAuthorizableByPath() throws RepositoryException, NotExecutableException { + String uid = superuser.getUserID(); + Authorizable a = userMgr.getAuthorizable(uid); + if (a == null) { + throw new NotExecutableException(); + } + try { + String path = a.getPath(); + Authorizable a2 = userMgr.getAuthorizableByPath(path); + assertNotNull(a2); + assertEquals(a.getID(), a2.getID()); + } catch (UnsupportedRepositoryOperationException e) { + throw new NotExecutableException(); + } + } + + @Test + public void testUserIDFromSession() throws RepositoryException, NotExecutableException { + User u = null; + Session uSession = null; + try { + String uid = createUserId(); + u = userMgr.createUser(uid, "pw"); + superuser.save(); + + uSession = superuser.getRepository().login(new SimpleCredentials(uid, "pw".toCharArray())); + assertEquals(u.getID(), uSession.getUserID()); + } finally { + if (uSession != null) { + uSession.logout(); + } + if (u != null) { + u.remove(); + superuser.save(); + } + } + } + + @Test + public void testCreateUserPrincipalNameEqualsUserID() throws RepositoryException, NotExecutableException { + User u = null; + try { + String uid = createUserId(); + u = userMgr.createUser(uid, "pw"); + superuser.save(); + + String msg = "User.getID() must return the userID pass to createUser."; + assertEquals(msg, uid, u.getID()); + + msg = "Principal name must be the same as userID."; + assertEquals(msg, uid, u.getPrincipal().getName()); + } finally { + if (u != null) { + u.remove(); + superuser.save(); + } + } + } + + @Test + public void testCreateUserIdDifferentFromPrincipalName() throws RepositoryException, NotExecutableException { + User u = null; + Session uSession = null; + try { + Principal p = getTestPrincipal(); + String uid = createUserId(); + + u = userMgr.createUser(uid, "pw", p, null); + superuser.save(); + + String msg = "Creating a User with principal-name distinct from Principal-name must succeed as long as both are unique."; + assertEquals(msg, u.getID(), uid); + assertEquals(msg, p.getName(), u.getPrincipal().getName()); + assertFalse(msg, u.getID().equals(u.getPrincipal().getName())); + + // make sure the userID exposed by a Session corresponding to that + // user is equal to the users ID. + uSession = superuser.getRepository().login(new SimpleCredentials(uid, "pw".toCharArray())); + assertEquals(uid, uSession.getUserID()); + } finally { + if (uSession != null) { + uSession.logout(); + } + if (u != null) { + u.remove(); + superuser.save(); + } + } + } + + @Test + public void testCreateGroupWithInvalidIdOrPrincipal() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = p.getName(); + + Principal emptyNamePrincipal = new Principal() { + @Override + public String getName() { + return ""; + } + }; + + Map fail = new HashMap(); + fail.put(uid, null); + fail.put(uid, emptyNamePrincipal); + fail.put(null, p); + fail.put("", p); + + for (String id : fail.keySet()) { + Group g = null; + try { + Principal princ = fail.get(id); + g = userMgr.createGroup(id, princ, null); + fail("Creating group with id '" + id + "' and principal '" + princ.getName() + "' should fail"); + } catch (IllegalArgumentException e) { + // success + } finally { + if (g != null) { + g.remove(); + superuser.save(); + } + } + } + } + + @Test + public void testCreateEveryoneUser() throws Exception { + User u = null; + try { + u = userMgr.createUser(EveryonePrincipal.NAME, "pw"); + fail("everyone is a reserved group name"); + } catch (IllegalArgumentException e) { + // success + } finally { + if (u != null) { + u.remove(); + } + } + + } + + /** + * @since oak + */ + @Test + public void testCreateUserWithoutPassword() throws Exception { + try { + User u = userMgr.createUser(createUserId(), null); + } finally { + superuser.refresh(false); + } + } + + @Test + public void testCreateGroupWithId() throws RepositoryException, NotExecutableException { + Group gr = null; + try { + String id = createGroupId(); + + // assert group creation with exact ID + gr = userMgr.createGroup(id); + superuser.save(); + assertEquals("Expect group with exact ID", id, gr.getID()); + + } finally { + if (gr != null) { + gr.remove(); + superuser.save(); + } + } + } + + @Test + public void testCreateGroupWithIdAndPrincipal() throws RepositoryException, NotExecutableException { + Group gr = null; + try { + Principal p = getTestPrincipal(); + String uid = p.getName(); + + // assert group creation with exact ID + gr = userMgr.createGroup(uid, p, null); + superuser.save(); + + assertEquals("Expect group with exact ID", uid, gr.getID()); + assertEquals("Expected group with exact principal name", p.getName(), gr.getPrincipal().getName()); + + } finally { + if (gr != null) { + gr.remove(); + superuser.save(); + } + } + } + + @Test + public void testCreateGroupIdDifferentFromPrincipalName() throws RepositoryException, NotExecutableException { + Group g = null; + try { + Principal p = getTestPrincipal(); + + g = userMgr.createGroup("testGroup", p, null); + superuser.save(); + + String msg = "Creating a Group with principal-name distinct from Principal-name must succeed as long as both are unique."; + assertEquals(msg, g.getID(), "testGroup"); + assertEquals(msg, p.getName(), g.getPrincipal().getName()); + assertFalse(msg, g.getID().equals(g.getPrincipal().getName())); + + } finally { + if (g != null) { + g.remove(); + superuser.save(); + } + } + } + + @Test + public void testCreateGroupWithExistingPrincipal() throws RepositoryException, NotExecutableException { + User u = null; + try { + Principal p = getTestPrincipal(); + String uid = p.getName(); + + // create a user with the given ID + u = userMgr.createUser(uid, "pw", p, null); + superuser.save(); + + // assert AuthorizableExistsException for principal that is already in use + Group gr = null; + try { + gr = userMgr.createGroup(p); + fail("Principal " + p.getName() + " is already in use -> must throw AuthorizableExistsException."); + } catch (AuthorizableExistsException aee) { + // expected this + } finally { + if (gr != null) { + gr.remove(); + superuser.save(); + } + } + + } finally { + if (u != null) { + u.remove(); + superuser.save(); + } + } + } + + @Test + public void testCreateGroupWithExistingPrincipal2() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = createUserId(); + + assertFalse(uid.equals(p.getName())); + + User u = null; + try { + // create a user with the given ID + u = userMgr.createUser(uid, "pw", p, null); + superuser.save(); + + // assert AuthorizableExistsException for principal that is already in use + Group gr = null; + try { + gr = userMgr.createGroup(p); + fail("Principal " + p.getName() + " is already in use -> must throw AuthorizableExistsException."); + } catch (AuthorizableExistsException aee) { + // expected this + } finally { + if (gr != null) { + gr.remove(); + superuser.save(); + } + } + + } finally { + if (u != null) { + u.remove(); + superuser.save(); + } + } + } + + @Test + public void testCreateGroupWithExistingPrincipal3() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = createUserId(); + + assertFalse(uid.equals(p.getName())); + + User u = null; + try { + // create a user with the given ID + u = userMgr.createUser(uid, "pw", p, null); + superuser.save(); + + // assert AuthorizableExistsException for principal that is already in use + Group gr = null; + try { + gr = userMgr.createGroup(createGroupId(), p, null); + fail("Principal " + p.getName() + " is already in use -> must throw AuthorizableExistsException."); + } catch (AuthorizableExistsException aee) { + // expected this + } finally { + if (gr != null) { + gr.remove(); + superuser.save(); + } + } + + } finally { + if (u != null) { + u.remove(); + superuser.save(); + } + } + } + + @Test + public void testCreateGroupWithExistingUserID() throws RepositoryException, NotExecutableException { + User u = null; + try { + String uid = createUserId(); + + // create a user with the given ID + u = userMgr.createUser(uid, "pw"); + superuser.save(); + + // assert AuthorizableExistsException for id that is already in use + Group gr = null; + try { + gr = userMgr.createGroup(uid); + fail("ID " + uid + " is already in use -> must throw AuthorizableExistsException."); + } catch (AuthorizableExistsException aee) { + // expected this + } finally { + if (gr != null) { + gr.remove(); + superuser.save(); + } + } + + } finally { + if (u != null) { + u.remove(); + superuser.save(); + } + } + } + + @Test + public void testCreateGroupWithExistingGroupID() throws RepositoryException, NotExecutableException { + Group g = null; + try { + String id = createGroupId(); + + // create a user with the given ID + g = userMgr.createGroup(id); + superuser.save(); + + // assert AuthorizableExistsException for id that is already in use + Group gr = null; + try { + gr = userMgr.createGroup(id); + fail("ID " + id + " is already in use -> must throw AuthorizableExistsException."); + } catch (AuthorizableExistsException aee) { + // expected this + } finally { + if (gr != null) { + gr.remove(); + superuser.save(); + } + } + + } finally { + if (g != null) { + g.remove(); + superuser.save(); + } + } + } + + @Test + public void testCreateGroupWithExistingGroupID2() throws RepositoryException, NotExecutableException { + + Group g = null; + try { + String id = createGroupId(); + + // create a group with the given ID + g = userMgr.createGroup(id); + superuser.save(); + + // assert AuthorizableExistsException for id that is already in use + Group gr = null; + try { + gr = userMgr.createGroup(id, getTestPrincipal(), null); + fail("ID " + id + " is already in use -> must throw AuthorizableExistsException."); + } catch (AuthorizableExistsException aee) { + // expected this + } finally { + if (gr != null) { + gr.remove(); + superuser.save(); + } + } + + } finally { + if (g != null) { + g.remove(); + superuser.save(); + } + } + } + + @Test + public void testFindAuthorizableByAddedProperty() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + Authorizable auth = null; + + try { + auth= userMgr.createGroup(p); + auth.setProperty("E-Mail", new Value[] { superuser.getValueFactory().createValue("anyVal")}); + superuser.save(); + + boolean found = false; + Iterator result = userMgr.findAuthorizables("E-Mail", "anyVal"); + while (result.hasNext()) { + Authorizable a = result.next(); + if (a.getID().equals(auth.getID())) { + found = true; + } + } + + assertTrue(found); + } finally { + // remove the create group again. + if (auth != null) { + auth.remove(); + superuser.save(); + } + } + } + + @Test + public void testFindAuthorizableByRelativePath() throws NotExecutableException, RepositoryException { + Principal p = getTestPrincipal(); + Authorizable auth = null; + + try { + auth= userMgr.createGroup(p); + Value[] vs = new Value[] { + superuser.getValueFactory().createValue("v1"), + superuser.getValueFactory().createValue("v2") + }; + + String relPath = "relPath/" + propertyName1; + String relPath2 = "another/" + propertyName1; + String relPath3 = "relPath/relPath/" + propertyName1; + auth.setProperty(relPath, vs); + auth.setProperty(relPath2, vs); + auth.setProperty(relPath3, superuser.getValueFactory().createValue("v3")); + superuser.save(); + + // relPath = "prop1", v = "v1" -> should find the target group + Iterator result = userMgr.findAuthorizables(propertyName1, "v1"); + assertTrue("expected result", result.hasNext()); + assertEquals(auth.getID(), result.next().getID()); + assertFalse("expected no more results", result.hasNext()); + + // relPath = "prop1", v = "v1" -> should find the target group + result = userMgr.findAuthorizables(propertyName1, "v3"); + assertTrue("expected result", result.hasNext()); + assertEquals(auth.getID(), result.next().getID()); + assertFalse("expected no more results", result.hasNext()); + + // relPath = "relPath/prop1", v = "v1" -> should find the target group + result = userMgr.findAuthorizables(relPath, "v1"); + assertTrue("expected result", result.hasNext()); + assertEquals(auth.getID(), result.next().getID()); + assertFalse("expected no more results", result.hasNext()); + + // relPath : "./prop1", v = "v1" -> should not find the target group + result = userMgr.findAuthorizables("./" + propertyName1, "v1"); + assertFalse("expected result", result.hasNext()); + + } finally { + // remove the create group again. + if (auth != null) { + auth.remove(); + superuser.save(); + } + } + } + + @Test + public void testFindUser() throws RepositoryException, NotExecutableException { + User u = null; + try { + Principal p = getTestPrincipal(); + String uid = createUserId(); + + u = userMgr.createUser(uid, "pw", p, null); + superuser.save(); + + boolean found = false; + Iterator it = userMgr.findAuthorizables(UserConstants.REP_PRINCIPAL_NAME, null, UserManager.SEARCH_TYPE_USER); + while (it.hasNext() && !found) { + User nu = (User) it.next(); + found = nu.getID().equals(uid); + } + assertTrue("Searching for 'null' must find the created user.", found); + + it = userMgr.findAuthorizables(UserConstants.REP_PRINCIPAL_NAME, p.getName(), UserManager.SEARCH_TYPE_USER); + found = false; + while (it.hasNext() && !found) { + User nu = (User) it.next(); + found = nu.getPrincipal().getName().equals(p.getName()); + } + assertTrue("Searching for principal-name must find the created user.", found); + + // but search groups should not find anything + it = userMgr.findAuthorizables(UserConstants.REP_PRINCIPAL_NAME, p.getName(), UserManager.SEARCH_TYPE_GROUP); + assertFalse(it.hasNext()); + + it = userMgr.findAuthorizables(UserConstants.REP_PRINCIPAL_NAME, null, UserManager.SEARCH_TYPE_GROUP); + while (it.hasNext()) { + if (it.next().getPrincipal().getName().equals(p.getName())) { + fail("Searching for Groups should never find a user"); + } + } + } finally { + if (u != null) { + u.remove(); + superuser.save(); + } + } + } + + @Test + public void testFindGroup() throws RepositoryException, NotExecutableException { + Group gr = null; + try { + Principal p = getTestPrincipal(); + gr = userMgr.createGroup(p); + superuser.save(); + + boolean found = false; + Iterator it = userMgr.findAuthorizables(UserConstants.REP_PRINCIPAL_NAME, null, UserManager.SEARCH_TYPE_GROUP); + while (it.hasNext() && !found) { + Group ng = (Group) it.next(); + found = ng.getPrincipal().getName().equals(p.getName()); + } + assertTrue("Searching for \"\" must find the created group.", found); + + it = userMgr.findAuthorizables(UserConstants.REP_PRINCIPAL_NAME, p.getName(), UserManager.SEARCH_TYPE_GROUP); + assertTrue(it.hasNext()); + Group ng = (Group) it.next(); + assertEquals("Searching for principal-name must find the created group.", p.getName(), ng.getPrincipal().getName()); + assertFalse("Only a single group must be found for a given principal name.", it.hasNext()); + + // but search users should not find anything + it = userMgr.findAuthorizables(UserConstants.REP_PRINCIPAL_NAME, p.getName(), UserManager.SEARCH_TYPE_USER); + assertFalse(it.hasNext()); + + it = userMgr.findAuthorizables(UserConstants.REP_PRINCIPAL_NAME, null, UserManager.SEARCH_TYPE_USER); + while (it.hasNext()) { + if (it.next().getPrincipal().getName().equals(p.getName())) { + fail("Searching for Users should never find a group"); + } + } + } finally { + if (gr != null) { + gr.remove(); + superuser.save(); + } + } + } + + @Test + public void testFindAllUsers() throws RepositoryException { + Iterator it = userMgr.findAuthorizables(UserConstants.REP_PRINCIPAL_NAME, null, UserManager.SEARCH_TYPE_USER); + while (it.hasNext()) { + assertFalse(it.next().isGroup()); + } + } + + @Test + public void testFindAllGroups() throws RepositoryException { + Iterator it = userMgr.findAuthorizables(UserConstants.REP_PRINCIPAL_NAME, null, UserManager.SEARCH_TYPE_GROUP); + while (it.hasNext()) { + assertTrue(it.next().isGroup()); + } + } + + @Test + public void testNewUserCanLogin() throws RepositoryException, NotExecutableException { + String uid = createUserId(); + User u = null; + Session s = null; + try { + u = userMgr.createUser(uid, "pw"); + superuser.save(); + + Credentials creds = new SimpleCredentials(uid, "pw".toCharArray()); + s = superuser.getRepository().login(creds); + } finally { + if (u != null) { + u.remove(); + superuser.save(); + } + if (s != null) { + s.logout(); + } + } + } + + @Test + public void testUnknownUserLogin() throws RepositoryException { + String uid = createUserId(); + assertNull(userMgr.getAuthorizable(uid)); + try { + Session s = superuser.getRepository().login(new SimpleCredentials(uid, uid.toCharArray())); + s.logout(); + + fail("An unknown user should not be allowed to execute the login."); + } catch (Exception e) { + // ok. + } + } + + @Test + public void testCleanup() throws RepositoryException, NotExecutableException { + Session s = getHelper().getSuperuserSession(); + try { + UserManager umgr = getUserManager(s); + s.logout(); + + // after logging out the session, the user manager must have been + // released as well and it's underlying session must not be available + // any more -> accessing users must fail. + try { + umgr.getAuthorizable("any userid"); + fail("After having logged out the original session, the user manager must not be live any more."); + } catch (RepositoryException e) { + // success + } + } finally { + if (s.isLive()) { + s.logout(); + } + } + } + + @Test + public void testCleanupForAllWorkspaces() throws RepositoryException, NotExecutableException { + String[] workspaceNames = superuser.getWorkspace().getAccessibleWorkspaceNames(); + + for (String workspaceName1 : workspaceNames) { + Session s = getHelper().getSuperuserSession(workspaceName1); + try { + UserManager umgr = getUserManager(s); + s.logout(); + + // after logging out the session, the user manager must have been + // released as well and it's underlying session must not be available + // any more -> accessing users must fail. + try { + umgr.getAuthorizable("any userid"); + fail("After having logged out the original session, the user manager must not be live any more."); + } catch (RepositoryException e) { + // success + } + } finally { + if (s.isLive()) { + s.logout(); + } + } + } + } + + @Test + public void testCreateWithRelativePath() throws Exception { + Principal p = getTestPrincipal(); + String uid = p.getName(); + + List invalid = new ArrayList(); + invalid.add("../../path"); + invalid.add(UserConstants.DEFAULT_USER_PATH + "/../test"); + invalid.add("../../../home/users/test"); + + for (String path : invalid) { + try { + User user = userMgr.createUser(uid, "pw", p, path); + superuser.save(); + + fail("intermediate path '"+ path +"' outside of the user tree."); + user.remove(); + superuser.save(); + + } catch (Exception e) { + // success + assertNull(userMgr.getAuthorizable(uid)); + } finally { + superuser.refresh(false); + } + } + } + + @Test + public void testCreateWithAbsoluteIntermediatePath() throws Exception { + Principal p = getTestPrincipal(); + String uid = p.getName(); + + User test = null; + try { + test = userMgr.createUser(uid, "pw", p, UserConstants.DEFAULT_USER_PATH + "/test"); + superuser.save(); + assertTrue(test.getPath().startsWith(UserConstants.DEFAULT_USER_PATH + "/test")); + } finally { + if (test != null) { + test.remove(); + superuser.save(); + } + } + } + + public void testAutoSave() throws RepositoryException, NotExecutableException { + if (userMgr.isAutoSave()) { + try { + userMgr.autoSave(false); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + } + + Principal p = getTestPrincipal(); + String uid = p.getName(); + User user = userMgr.createUser(uid, "pw"); + + String gid = createGroupId(); + Group group = userMgr.createGroup(gid); + superuser.refresh(false); + + // transient changes must be gone after the refresh-call. + assertNull(userMgr.getAuthorizable(uid)); + assertNull(userMgr.getAuthorizable(p)); + assertNull(userMgr.getAuthorizable(gid)); + } +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserQueryTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserQueryTest.java new file mode 100644 index 00000000000..5294bdf2504 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserQueryTest.java @@ -0,0 +1,827 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.security.user; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import com.google.common.base.Predicate; +import com.google.common.collect.Iterators; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.Query; +import org.apache.jackrabbit.api.security.user.QueryBuilder; +import org.apache.jackrabbit.api.security.user.User; +import org.junit.Test; + +public class UserQueryTest extends AbstractUserTest { + + private User kangaroo; + private User elephant; + private User goldenToad; + + private final Set users = new HashSet(); + + private Group vertebrates; + private Group mammals; + private Group apes; + + private final Set groups = new HashSet(); + private final Set authorizables = new HashSet(); + + private final Set systemDefined = new HashSet(); + + @Override + public void setUp() throws Exception { + super.setUp(); + + Iterator systemAuthorizables = userMgr.findAuthorizables("rep:principalName", null); + while (systemAuthorizables.hasNext()) { + Authorizable authorizable = systemAuthorizables.next(); + if (authorizable.isGroup()) { + groups.add((Group) authorizable); + } + else { + users.add((User) authorizable); + } + systemDefined.add(authorizable); + } + + Group animals = createGroup("animals"); + Group invertebrates = createGroup("invertebrates"); + Group arachnids = createGroup("arachnids"); + Group insects = createGroup("insects"); + vertebrates = createGroup("vertebrates"); + mammals = createGroup("mammals"); + apes = createGroup("apes"); + Group reptiles = createGroup("reptiles"); + Group birds = createGroup("birds"); + Group amphibians = createGroup("amphibians"); + + animals.addMember(invertebrates); + animals.addMember(vertebrates); + invertebrates.addMember(arachnids); + invertebrates.addMember(insects); + vertebrates.addMember(mammals); + vertebrates.addMember(reptiles); + vertebrates.addMember(birds); + vertebrates.addMember(amphibians); + mammals.addMember(apes); + + User blackWidow = createUser("black widow", "flies", 2, false); + User gardenSpider = createUser("garden spider", "flies", 2, false); + User jumpingSpider = createUser("jumping spider", "insects", 1, false); + addMembers(arachnids, blackWidow, gardenSpider, jumpingSpider); + + User ant = createUser("ant", "leaves", 0.5, false); + User bee = createUser("bee", "honey", 2.5, true); + User fly = createUser("fly", "dirt", 1.3, false); + addMembers(insects, ant, bee, fly); + + User jackrabbit = createUser("jackrabbit", "carrots", 2500, true); + User deer = createUser("deer", "leaves", 120000, true); + User opposum = createUser("opposum", "fruit", 1200, true); + kangaroo = createUser("kangaroo", "grass", 90000, true); + elephant = createUser("elephant", "leaves", 5000000, true); + addMembers(mammals, jackrabbit, deer, opposum, kangaroo, elephant); + + User lemur = createUser("lemur", "nectar", 1100, true); + User gibbon = createUser("gibbon", "meat", 20000, true); + addMembers(apes, lemur, gibbon); + + User crocodile = createUser("crocodile", "meat", 80000, false); + User turtle = createUser("turtle", "leaves", 10000, true); + User lizard = createUser("lizard", "leaves", 1900, false); + addMembers(reptiles, crocodile, turtle, lizard); + + User kestrel = createUser("kestrel", "mice", 2000, false); + User goose = createUser("goose", "snails", 13000, true); + User pelican = createUser("pelican", "fish", 15000, true); + User dove = createUser("dove", "insects", 1600, false); + addMembers(birds, kestrel, goose, pelican, dove); + + User salamander = createUser("salamander", "insects", 800, true); + goldenToad = createUser("golden toad", "insects", 700, false); + User poisonDartFrog = createUser("poison dart frog", "insects", 40, false); + addMembers(amphibians, salamander, goldenToad, poisonDartFrog); + + setProperty("canFly", vf.createValue(true), bee, fly, kestrel, goose, pelican, dove); + setProperty("poisonous",vf.createValue(true), blackWidow, bee, poisonDartFrog); + setProperty("poisonous", vf.createValue(false), turtle, lemur); + setProperty("hasWings", vf.createValue(false), blackWidow, gardenSpider, jumpingSpider, ant, + jackrabbit, deer, opposum, kangaroo, elephant, lemur, gibbon, crocodile, turtle, lizard, + salamander, goldenToad, poisonDartFrog); + setProperty("color", vf.createValue("black"), blackWidow, gardenSpider, ant, fly, lizard, salamander); + setProperty("color", vf.createValue("WHITE"), opposum, goose, pelican, dove); + setProperty("color", vf.createValue("gold"), goldenToad); + setProperty("numberOfLegs", vf.createValue(2), kangaroo, gibbon, kestrel, goose, dove); + setProperty("numberOfLegs", vf.createValue(4), jackrabbit, deer, opposum, elephant, lemur, crocodile, + turtle, lizard, salamander, goldenToad, poisonDartFrog); + setProperty("numberOfLegs", vf.createValue(6), ant, bee, fly); + setProperty("numberOfLegs", vf.createValue(8), blackWidow, gardenSpider, jumpingSpider); + + elephant.getImpersonation().grantImpersonation(jackrabbit.getPrincipal()); + + authorizables.addAll(users); + authorizables.addAll(groups); + + if (!userMgr.isAutoSave()) { + superuser.save(); + } + + } + + @Override + public void tearDown() throws Exception { + for (Authorizable authorizable : authorizables) { + if (!systemDefined.contains(authorizable)) { + authorizable.remove(); + } + } + authorizables.clear(); + groups.clear(); + users.clear(); + + if (!userMgr.isAutoSave()) { + superuser.save(); + } + + super.tearDown(); + } + + @Test + public void testAny() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { /* any */ } + }); + + assertSameElements(result, authorizables.iterator()); + } + + @Test + public void testSelector() throws RepositoryException { + List> selectors = new ArrayList>(); + selectors.add(Authorizable.class); + selectors.add(Group.class); + selectors.add(User.class); + + for (final Class s : selectors) { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setSelector(s); + } + }); + + if (User.class.isAssignableFrom(s)) { + assertSameElements(result, users.iterator()); + } + else if (Group.class.isAssignableFrom(s)) { + assertSameElements(result, groups.iterator()); + } + else { + assertSameElements(result, authorizables.iterator()); + } + } + } + + @Test + public void testDirectScope() throws RepositoryException { + Group[] groups = new Group[]{mammals, vertebrates, apes}; + for (final Group g : groups) { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + try { + builder.setScope(g.getID(), true); + } catch (RepositoryException e) { + fail(e.getMessage()); + } + } + }); + + Iterator members = g.getDeclaredMembers(); + assertSameElements(result, members); + } + } + + @Test + public void testIndirectScope() throws RepositoryException { + Group[] groups = new Group[]{mammals, vertebrates, apes}; + for (final Group g : groups) { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + try { + builder.setScope(g.getID(), false); + } catch (RepositoryException e) { + fail(e.getMessage()); + } + } + }); + + Iterator members = g.getMembers(); + assertSameElements(result, members); + } + } + + @Test + public void testFindUsersInGroup() throws RepositoryException { + Group[] groups = new Group[]{mammals, vertebrates, apes}; + for (final Group g : groups) { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + try { + builder.setSelector(User.class); + builder.setScope(g.getID(), false); + } catch (RepositoryException e) { + fail(e.getMessage()); + } + } + }); + + Iterator members = g.getMembers(); + Iterator users = Iterators.filter(members, new Predicate() { + public boolean apply(Authorizable authorizable) { + return !authorizable.isGroup(); + } + }); + assertSameElements(result, users); + } + } + + @Test + public void testFindGroupsInGroup() throws RepositoryException { + Group[] groups = new Group[]{mammals, vertebrates, apes}; + for (final Group g : groups) { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + try { + builder.setSelector(Group.class); + builder.setScope(g.getID(), true); + } catch (RepositoryException e) { + fail(e.getMessage()); + } + } + }); + + Iterator members = g.getDeclaredMembers(); + Iterator users = Iterators.filter(members, new Predicate() { + public boolean apply(Authorizable authorizable) { + return authorizable.isGroup(); + } + }); + assertSameElements(result, users); + } + } + + @Test + public void testNameMatch() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder.nameMatches("a%")); + } + }); + + Iterator expected = Iterators.filter(authorizables.iterator(), new Predicate() { + public boolean apply(Authorizable authorizable) { + try { + String name = authorizable.getID(); + Principal principal = authorizable.getPrincipal(); + return name.startsWith("a") || principal != null && principal.getName().startsWith("a"); + } catch (RepositoryException e) { + fail(e.getMessage()); + } + return false; + } + }); + + assertTrue(result.hasNext()); + assertSameElements(result, expected); + } + + @Test + public void testFindProperty1() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder. + eq("@canFly", vf.createValue(true))); + } + }); + + Iterator expected = Iterators.filter(users.iterator(), new Predicate() { + public boolean apply(User user) { + try { + Value[] canFly = user.getProperty("canFly"); + return canFly != null && canFly.length == 1 && canFly[0].getBoolean(); + } catch (RepositoryException e) { + fail(e.getMessage()); + } + return false; + } + }); + + assertTrue(result.hasNext()); + assertSameElements(result, expected); + } + + @Test + public void testFindProperty2() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder. + gt("profile/@weight", vf.createValue(2000.0))); + } + }); + + Iterator expected = Iterators.filter(users.iterator(), new Predicate() { + public boolean apply(User user) { + try { + Value[] weight = user.getProperty("profile/weight"); + return weight != null && weight.length == 1 && weight[0].getDouble() > 2000.0; + } catch (RepositoryException e) { + fail(e.getMessage()); + } + return false; + } + }); + + assertTrue(result.hasNext()); + assertSameElements(result, expected); + } + + @Test + public void testFindProperty3() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder. + eq("@numberOfLegs", vf.createValue(8))); + } + }); + + Iterator expected = Iterators.filter(users.iterator(), new Predicate() { + public boolean apply(User user) { + try { + Value[] numberOfLegs = user.getProperty("numberOfLegs"); + return numberOfLegs != null && numberOfLegs.length == 1 && numberOfLegs[0].getLong() == 8; + } catch (RepositoryException e) { + fail(e.getMessage()); + } + return false; + } + }); + + assertTrue(result.hasNext()); + assertSameElements(result, expected); + } + + @Test + public void testPropertyExistence() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder. + exists("@poisonous")); + } + }); + + Iterator expected = Iterators.filter(users.iterator(), new Predicate() { + public boolean apply(User user) { + try { + Value[] poisonous = user.getProperty("poisonous"); + return poisonous != null && poisonous.length == 1; + } catch (RepositoryException e) { + fail(e.getMessage()); + } + return false; + } + }); + + assertTrue(result.hasNext()); + assertSameElements(result, expected); + } + + @Test + public void testPropertyLike() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder. + like("profile/@food", "m%")); + } + }); + + Iterator expected = Iterators.filter(users.iterator(), new Predicate() { + public boolean apply(User user) { + try { + Value[] food = user.getProperty("profile/food"); + if (food == null || food.length != 1) { + return false; + } + else { + String value = food[0].getString(); + return value.startsWith("m"); + } + } catch (RepositoryException e) { + fail(e.getMessage()); + } + return false; + } + }); + + assertTrue(result.hasNext()); + assertSameElements(result, expected); + } + + @Test + public void testContains1() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder. + contains(".", "gold")); + } + }); + + Iterator expected = Iterators.singletonIterator(goldenToad); + assertTrue(result.hasNext()); + assertSameElements(result, expected); + } + + @Test + public void testContains2() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder. + contains("@color", "gold")); + } + }); + + Iterator expected = Iterators.singletonIterator(goldenToad); + assertTrue(result.hasNext()); + assertSameElements(result, expected); + } + + @Test + public void testContains3() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder. + contains("profile/.", "grass")); + } + }); + + Iterator expected = Iterators.singletonIterator(kangaroo); + assertTrue(result.hasNext()); + assertSameElements(result, expected); + } + + @Test + public void testContains4() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder. + contains("profile/@food", "grass")); + } + }); + + Iterator expected = Iterators.singletonIterator(kangaroo); + assertTrue(result.hasNext()); + assertSameElements(result, expected); + } + + @Test + public void testCondition1() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder. + and(builder. + eq("profile/@cute", vf.createValue(true)), builder. + not(builder. + eq("@color", vf.createValue("black"))))); + } + }); + + Iterator expected = Iterators.filter(users.iterator(), new Predicate() { + public boolean apply(User user) { + try { + Value[] cute = user.getProperty("profile/cute"); + Value[] black = user.getProperty("color"); + return cute != null && cute.length == 1 && cute[0].getBoolean() && + !(black != null && black.length == 1 && black[0].getString().equals("black")); + } catch (RepositoryException e) { + fail(e.getMessage()); + } + return false; + } + }); + + assertTrue(result.hasNext()); + assertSameElements(result, expected); + } + + @Test + public void testCondition2() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder. + or(builder. + eq("profile/@food", vf.createValue("mice")), builder. + eq("profile/@food", vf.createValue("nectar")))); + } + }); + + Iterator expected = Iterators.filter(users.iterator(), new Predicate() { + public boolean apply(User user) { + try { + Value[] food = user.getProperty("profile/food"); + return food != null && food.length == 1 && + (food[0].getString().equals("mice") || food[0].getString().equals("nectar")); + } catch (RepositoryException e) { + fail(e.getMessage()); + } + return false; + } + }); + + assertTrue(result.hasNext()); + assertSameElements(result, expected); + } + + @Test + public void testImpersonation() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder. + impersonates("jackrabbit")); + } + }); + + Iterator expected = Iterators.singletonIterator(elephant); + assertTrue(result.hasNext()); + assertSameElements(result, expected); + } + + @Test + public void testSortOrder1() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder. + exists("@color")); + builder.setSortOrder("@color", QueryBuilder.Direction.DESCENDING); + } + }); + + assertTrue(result.hasNext()); + String prev = null; + while (result.hasNext()) { + Authorizable authorizable = result.next(); + Value[] color = authorizable.getProperty("color"); + assertNotNull(color); + assertEquals(1, color.length); + assertTrue(prev == null || prev.compareToIgnoreCase(color[0].getString()) >= 0); + prev = color[0].getString(); + } + } + + @Test + public void testSortOrder2() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder. + exists("profile/@weight")); + builder.setSortOrder("profile/@weight", QueryBuilder.Direction.ASCENDING, true); + } + }); + + assertTrue(result.hasNext()); + double prev = Double.MIN_VALUE; + while (result.hasNext()) { + Authorizable authorizable = result.next(); + Value[] weight = authorizable.getProperty("profile/weight"); + assertNotNull(weight); + assertEquals(1, weight.length); + assertTrue(prev <= weight[0].getDouble()); + prev = weight[0].getDouble(); + } + } + + @Test + public void testOffset() throws RepositoryException { + long[] offsets = {2, 0, 3, 0, 100000}; + long[] counts = {4, 4, 0, 100000, 100000}; + + for (int k = 0; k < offsets.length; k++) { + final long offset = offsets[k]; + final long count = counts[k]; + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setSortOrder("profile/@weight", QueryBuilder.Direction.ASCENDING); + builder.setLimit(offset, count); + } + }); + + Iterator expected = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setSortOrder("profile/@weight", QueryBuilder.Direction.ASCENDING); + } + }); + + skip(expected, offset); + assertSame(expected, result, count); + assertFalse(result.hasNext()); + } + } + + @Test + public void testSetBound() throws RepositoryException { + List sortedUsers = new ArrayList(users); + sortedUsers.removeAll(systemDefined); // remove system defined users: no @weight + + Comparator comp = new Comparator() { + public int compare(User user1, User user2) { + try { + Value[] weight1 = user1.getProperty("profile/weight"); + assertNotNull(weight1); + assertEquals(1, weight1.length); + + Value[] weight2 = user2.getProperty("profile/weight"); + assertNotNull(weight2); + assertEquals(1, weight2.length); + + return weight1[0].getDouble() < weight2[0].getDouble() ? -1 : 1; + } catch (RepositoryException e) { + fail(e.getMessage()); + return 0; // Make the compiler happy + } + } + }; + Collections.sort(sortedUsers, comp); + + long[] counts = {4, 0, 100000}; + for (final long count : counts) { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder. + eq("profile/@cute", vf.createValue(true))); + builder.setSortOrder("profile/@weight", QueryBuilder.Direction.ASCENDING, true); + builder.setLimit(vf.createValue(1000.0), count); + } + }); + + Iterator expected = Iterators.filter(sortedUsers.iterator(), new Predicate() { + public boolean apply(User user) { + try { + Value[] cute = user.getProperty("profile/cute"); + Value[] weight = user.getProperty("profile/weight"); + return cute != null && cute.length == 1 && cute[0].getBoolean() && + weight != null && weight.length == 1 && weight[0].getDouble() > 1000.0; + + } catch (RepositoryException e) { + fail(e.getMessage()); + } + return false; + } + }); + + assertSame(expected, result, count); + assertFalse(result.hasNext()); + } + } + + @Test + public void testScopeWithOffset() throws RepositoryException { + final int offset = 5; + final int count = 10000; + + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setScope("vertebrates", false); + builder.setSortOrder("profile/@weight", QueryBuilder.Direction.ASCENDING); + builder.setLimit(offset, count); + } + }); + + Iterator expected = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setScope("vertebrates", false); + builder.setSortOrder("profile/@weight", QueryBuilder.Direction.ASCENDING); + } + }); + + skip(expected, offset); + assertSame(expected, result, count); + assertFalse(result.hasNext()); + } + + @Test + public void testScopeWithMax() throws RepositoryException { + final int offset = 0; + final int count = 22; + + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setScope("vertebrates", false); + builder.setSortOrder("profile/@weight", QueryBuilder.Direction.ASCENDING); + builder.setLimit(offset, count); + } + }); + + Iterator expected = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setScope("vertebrates", false); + builder.setSortOrder("profile/@weight", QueryBuilder.Direction.ASCENDING); + } + }); + + assertSameElements(expected, result); + assertFalse(result.hasNext()); + } + + //------------------------------------------------------------< private >--- + + private static void addMembers(Group group, Authorizable... authorizables) throws RepositoryException { + for (Authorizable authorizable : authorizables) { + group.addMember(authorizable); + } + } + + private Group createGroup(String name) throws RepositoryException { + Group group = userMgr.createGroup(name); + groups.add(group); + return group; + } + + private User createUser(String name, String food, double weight, boolean cute) throws RepositoryException { + User user = userMgr.createUser(name, ""); + user.setProperty("profile/food", vf.createValue(food)); + user.setProperty("profile/weight", vf.createValue(weight)); + user.setProperty("profile/cute", vf.createValue(cute)); + users.add(user); + return user; + } + + private static void setProperty(String relPath, Value value, Authorizable... authorizables) throws RepositoryException { + for (Authorizable authorizable : authorizables) { + authorizable.setProperty(relPath, value); + } + } + + private static void assertSameElements(Iterator it1, Iterator it2) { + Set set1 = toSet(it1); + Set set2 = toSet(it2); + + Set missing = new HashSet(); + missing.addAll(set2); + missing.removeAll(set1); + + Set excess = new HashSet(); + excess.addAll(set1); + excess.removeAll(set2); + + if (!missing.isEmpty()) { + fail("Missing elements in query result: " + missing); + } + + if (!excess.isEmpty()) { + fail("Excess elements in query result: " + excess); + } + } + + private static Set toSet(Iterator it) { + Set set = new HashSet(); + while (it.hasNext()) { + set.add(it.next()); + } + return set; + } + + private static void assertSame(Iterator expected, Iterator actual, long count) { + for (int k = 0; k < count && actual.hasNext(); k++) { + assertTrue(expected.hasNext()); + assertEquals(expected.next(), actual.next()); + } + } + + private static void skip(Iterator iterator, long count) { + for (int k = 0; k < count && iterator.hasNext(); k++) { + iterator.next(); + } + } +} \ No newline at end of file diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserTest.java new file mode 100644 index 00000000000..29cdbeebebf --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserTest.java @@ -0,0 +1,281 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr.security.user; + +import java.security.Principal; +import javax.jcr.Credentials; +import javax.jcr.LoginException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.UnsupportedRepositoryOperationException; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.apache.jackrabbit.test.NotExecutableException; +import org.junit.Test; + +/** + * UserTest... + */ +public class UserTest extends AbstractUserTest { + + @Test + public void testIsUser() throws RepositoryException { + Authorizable authorizable = userMgr.getAuthorizable(user.getID()); + assertTrue(authorizable instanceof User); + } + + @Test + public void testIsGroup() throws RepositoryException { + assertFalse(user.isGroup()); + } + + @Test + public void testGetId() throws NotExecutableException, RepositoryException { + assertNotNull(user.getID()); + assertNotNull(userMgr.getAuthorizable(user.getID()).getID()); + } + + @Test + public void testGetPrincipal() throws RepositoryException, NotExecutableException { + assertNotNull(user.getPrincipal()); + assertNotNull(userMgr.getAuthorizable(user.getID()).getPrincipal()); + } + + @Test + public void testGetPath() throws RepositoryException, NotExecutableException { + assertNotNull(user.getPath()); + assertNotNull(userMgr.getAuthorizable(user.getID()).getPath()); + try { + assertEquals(getNode(user, superuser).getPath(), user.getPath()); + } catch (UnsupportedRepositoryOperationException e) { + // ok. + } + } + + @Test + public void testIsAdmin() throws NotExecutableException, RepositoryException { + assertFalse(user.isAdmin()); + } + + @Test + public void testChangePasswordNull() throws RepositoryException, NotExecutableException { + // invalid 'null' pw string + try { + user.changePassword(null); + superuser.save(); + fail("invalid pw null"); + } catch (Exception e) { + // success + } + } + + @Test + public void testChangePassword() throws RepositoryException, NotExecutableException { + try { + String hash = getNode(user, superuser).getProperty(UserConstants.REP_PASSWORD).getString(); + + user.changePassword("changed"); + superuser.save(); + + assertFalse(hash.equals(getNode(user, superuser).getProperty(UserConstants.REP_PASSWORD).getString())); + } catch (Exception e) { + // success + } + } + + @Test + public void testChangePasswordWithInvalidOldPassword() throws RepositoryException, NotExecutableException { + try { + user.changePassword("changed", "wrongOldPw"); + superuser.save(); + fail("old password didn't match -> changePassword(String,String) should fail."); + } catch (RepositoryException e) { + // success. + } + } + + @Test + public void testChangePasswordWithOldPassword() throws RepositoryException, NotExecutableException { + try { + String hash = getNode(user, superuser).getProperty(UserConstants.REP_PASSWORD).getString(); + + user.changePassword("changed", testPw); + superuser.save(); + + assertFalse(hash.equals(getNode(user, superuser).getProperty(UserConstants.REP_PASSWORD).getString())); + } finally { + user.changePassword(testPw); + superuser.save(); + } + } + + @Test + public void testLoginAfterChangePassword() throws RepositoryException { + user.changePassword("changed"); + superuser.save(); + + // make sure the user can login with the new pw + Session s = getHelper().getRepository().login(new SimpleCredentials(user.getID(), "changed".toCharArray())); + s.logout(); + } + + @Test + public void testLoginAfterChangePassword2() throws RepositoryException, NotExecutableException { + try { + + user.changePassword("changed", testPw); + superuser.save(); + + // make sure the user can login with the new pw + Session s = getHelper().getRepository().login(new SimpleCredentials(user.getID(), "changed".toCharArray())); + s.logout(); + } finally { + user.changePassword(testPw); + superuser.save(); + } + } + + @Test + public void testLoginWithOldPassword() throws RepositoryException, NotExecutableException { + try { + user.changePassword("changed"); + superuser.save(); + + Session s = getHelper().getRepository().login(new SimpleCredentials(user.getID(), testPw.toCharArray())); + s.logout(); + fail("user pw has changed. login must fail."); + } catch (LoginException e) { + // success + } + } + + @Test + public void testLoginWithOldPassword2() throws RepositoryException, NotExecutableException { + try { + user.changePassword("changed", testPw); + superuser.save(); + + Session s = getHelper().getRepository().login(new SimpleCredentials(user.getID(), testPw.toCharArray())); + s.logout(); + fail("superuser pw has changed. login must fail."); + } catch (LoginException e) { + // success + } finally { + user.changePassword(testPw); + superuser.save(); + } + } + + @Test + public void testEnabledByDefault() throws Exception { + // by default a user isn't disabled + assertFalse(user.isDisabled()); + assertNull(user.getDisabledReason()); + } + + @Test + public void testDisable() throws Exception { + String reason = "readonly user is disabled!"; + user.disable(reason); + superuser.save(); + assertTrue(user.isDisabled()); + assertEquals(reason, user.getDisabledReason()); + } + + @Test + public void testAccessDisabledUser() throws Exception { + user.disable("readonly user is disabled!"); + superuser.save(); + + // user must still be retrievable from user manager + assertNotNull(getUserManager(superuser).getAuthorizable(user.getID())); + // ... and from principal manager as well + assertTrue(((JackrabbitSession) superuser).getPrincipalManager().hasPrincipal(user.getPrincipal().getName())); + } + + @Test + public void testAccessPrincipalOfDisabledUser() throws Exception { + user.disable("readonly user is disabled!"); + superuser.save(); + + Principal principal = user.getPrincipal(); + assertTrue(((JackrabbitSession) superuser).getPrincipalManager().hasPrincipal(principal.getName())); + assertEquals(principal, ((JackrabbitSession) superuser).getPrincipalManager().getPrincipal(principal.getName())); + } + + @Test + public void testEnableUser() throws Exception { + user.disable("readonly user is disabled!"); + superuser.save(); + + // enable user again + user.disable(null); + superuser.save(); + assertFalse(user.isDisabled()); + assertNull(user.getDisabledReason()); + + // -> login must succeed again + getHelper().getRepository().login(new SimpleCredentials(user.getID(), "pw".toCharArray())).logout(); + } + + @Test + public void testLoginDisabledUser() throws Exception { + user.disable("readonly user is disabled!"); + superuser.save(); + + // -> login must fail + try { + Session ss = getHelper().getRepository().login(new SimpleCredentials(user.getID(), "pw".toCharArray())); + ss.logout(); + fail("A disabled user must not be allowed to login any more"); + } catch (LoginException e) { + // success + } + } + + @Test + public void testImpersonateDisabledUser() throws Exception { + user.disable("readonly user is disabled!"); + superuser.save(); + + // -> impersonating this user must fail + try { + Session ss = superuser.impersonate(new SimpleCredentials(user.getID(), new char[0])); + ss.logout(); + fail("A disabled user cannot be impersonated any more."); + } catch (LoginException e) { + // success + } + } + + public void testLoginWithGetCredentials() throws RepositoryException, NotExecutableException { + try { + Credentials creds = user.getCredentials(); + Session s = getHelper().getRepository().login(creds); + s.logout(); + fail("Login using credentials exposed on user must fail."); + } catch (UnsupportedRepositoryOperationException e) { + throw new NotExecutableException(e.getMessage()); + } catch (LoginException e) { + // success + } + } +} \ No newline at end of file diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/ApiIT.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/ApiIT.java new file mode 100644 index 00000000000..84e7e5234a9 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/ApiIT.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.jcr.tck; + +import junit.framework.Test; +import org.apache.jackrabbit.test.ConcurrentTestSuite; + +public class ApiIT extends ConcurrentTestSuite { + + public static Test suite() { + return new ApiIT(); + } + + public ApiIT() { + super("JCR API tests"); + addTest(org.apache.jackrabbit.test.api.TestAll.suite()); + } + +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/LockIT.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/LockIT.java new file mode 100644 index 00000000000..7a13302e357 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/LockIT.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.jcr.tck; + +import junit.framework.Test; +import org.apache.jackrabbit.test.ConcurrentTestSuite; + +public class LockIT extends ConcurrentTestSuite { + + public static Test suite() { + return new LockIT(); + } + + public LockIT() { + super("JCR lock tests"); + addTest(org.apache.jackrabbit.test.api.lock.TestAll.suite()); + } +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/NodetypeIT.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/NodetypeIT.java new file mode 100644 index 00000000000..12999dc8632 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/NodetypeIT.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.jcr.tck; + +import junit.framework.Test; +import org.apache.jackrabbit.test.ConcurrentTestSuite; + +public class NodetypeIT extends ConcurrentTestSuite { + + public static Test suite() { + return new NodetypeIT(); + } + + public NodetypeIT() { + super("JCR node type tests"); + addTest(org.apache.jackrabbit.test.api.nodetype.TestAll.suite()); + } +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/ObservationIT.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/ObservationIT.java new file mode 100644 index 00000000000..8c9777f55cc --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/ObservationIT.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.jcr.tck; + +import junit.framework.Test; +import org.apache.jackrabbit.test.ConcurrentTestSuite; + +public class ObservationIT extends ConcurrentTestSuite { + + public static Test suite() { + return new ObservationIT(); + } + + public ObservationIT() { + super("JCR observation tests"); + addTest(org.apache.jackrabbit.test.api.observation.TestAll.suite()); + } +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/QueryIT.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/QueryIT.java new file mode 100644 index 00000000000..c8bd43ddda1 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/QueryIT.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.jcr.tck; + +import junit.framework.Test; +import org.apache.jackrabbit.test.ConcurrentTestSuite; + +public class QueryIT extends ConcurrentTestSuite { + + public static Test suite() { + return new QueryIT(); + } + + public QueryIT() { + super("JCR query tests"); + addTest(org.apache.jackrabbit.test.api.query.TestAll.suite()); + addTest(org.apache.jackrabbit.test.api.query.qom.TestAll.suite()); + } +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/RetentionIT.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/RetentionIT.java new file mode 100644 index 00000000000..4df3252d55c --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/RetentionIT.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.jcr.tck; + +import junit.framework.Test; +import org.apache.jackrabbit.test.ConcurrentTestSuite; + +public class RetentionIT extends ConcurrentTestSuite { + + public static Test suite() { + return new RetentionIT(); + } + + public RetentionIT() { + super("JCR retention tests"); + addTest(org.apache.jackrabbit.test.api.retention.TestAll.suite()); + } +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/SecurityIT.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/SecurityIT.java new file mode 100644 index 00000000000..3a20fdb35fa --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/SecurityIT.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.jcr.tck; + +import junit.framework.Test; +import org.apache.jackrabbit.test.ConcurrentTestSuite; + +public class SecurityIT extends ConcurrentTestSuite { + + public static Test suite() { + return new SecurityIT(); + } + + public SecurityIT() { + super("JCR security tests"); + addTest(org.apache.jackrabbit.test.api.security.TestAll.suite()); + } +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/VersionIT.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/VersionIT.java new file mode 100644 index 00000000000..1ca3a4526c4 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/tck/VersionIT.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.jcr.tck; + +import junit.framework.Test; +import org.apache.jackrabbit.test.ConcurrentTestSuite; + +public class VersionIT extends ConcurrentTestSuite { + + public static Test suite() { + return new VersionIT(); + } + + public VersionIT() { + super("JCR version tests"); + addTest(org.apache.jackrabbit.test.api.version.TestAll.suite()); + addTest(org.apache.jackrabbit.test.api.version.simple.TestAll.suite()); } +} diff --git a/oak-jcr/src/test/resources/logback-test.xml b/oak-jcr/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..1d3e2f64008 --- /dev/null +++ b/oak-jcr/src/test/resources/logback-test.xml @@ -0,0 +1,39 @@ + + + + + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + target/unit-tests.log + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + + + + + diff --git a/oak-jcr/src/test/resources/org/apache/jackrabbit/oak/jcr/test_nodetypes.cnd b/oak-jcr/src/test/resources/org/apache/jackrabbit/oak/jcr/test_nodetypes.cnd new file mode 100644 index 00000000000..b782a0bd81f --- /dev/null +++ b/oak-jcr/src/test/resources/org/apache/jackrabbit/oak/jcr/test_nodetypes.cnd @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +<'test'='http://www.apache.org/jackrabbit/test'> + +[test:canAddChildNode] + + testChildWithDefaultType (nt:base) = nt:base + + testChildWithoutDefaultType (nt:base) + +[test:canSetProperty] + - BinaryMultipleConstraints (binary) multiple nofulltext noqueryorder < '(,100)' + - Boolean (boolean) nofulltext noqueryorder + - DateConstraints (date) nofulltext noqueryorder < '(1974-02-15T00:00:00.000Z,)' + - NameConstraints (name) nofulltext noqueryorder < 'abc' + - LongMultipleConstraints (long) multiple nofulltext noqueryorder < '(,100)' + - ReferenceMultipleConstraints (reference) multiple nofulltext noqueryorder < 'test:canSetProperty' + - StringMultiple (string) multiple nofulltext noqueryorder + - DoubleMultiple (double) multiple nofulltext noqueryorder + - DoubleConstraints (double) nofulltext noqueryorder < '(100,)' + - BinaryConstraints (binary) nofulltext noqueryorder < '(,100)' + - PathMultiple (path) multiple nofulltext noqueryorder + - Path (path) nofulltext noqueryorder + - StringMultipleConstraints (string) multiple nofulltext noqueryorder < 'abc', 'def', 'ghi' + - PathMultipleConstraints (path) multiple nofulltext noqueryorder < '/abc' + - DateMultiple (date) multiple nofulltext noqueryorder + - Binary (binary) nofulltext noqueryorder + - LongMultiple (long) multiple nofulltext noqueryorder + - LongConstraints (long) nofulltext noqueryorder < '(100,)' + - BooleanConstraints (boolean) nofulltext noqueryorder < 'true' + - BinaryMultiple (binary) multiple nofulltext noqueryorder + - Long (long) nofulltext noqueryorder + - StringConstraints (string) nofulltext noqueryorder < 'abc', 'def', 'ghi' + - Date (date) nofulltext noqueryorder + - ReferenceConstraints (reference) nofulltext noqueryorder < 'test:canSetProperty' + - Double (double) nofulltext noqueryorder + - NameMultipleConstraints (name) multiple nofulltext noqueryorder < 'abc' + - BooleanMultiple (boolean) multiple nofulltext noqueryorder + - Name (name) nofulltext noqueryorder + - PathConstraints (path) nofulltext noqueryorder < '/abc' + - String (string) nofulltext noqueryorder + - NameMultiple (name) multiple nofulltext noqueryorder + - BooleanMultipleConstraints (boolean) multiple nofulltext noqueryorder < 'true' + - DateMultipleConstraints (date) multiple nofulltext noqueryorder < '(,1974-02-15T00:00:00.000Z)' + - DoubleMultipleConstraints (double) multiple nofulltext noqueryorder < '(,100)' + +[test:refTargetNode] > mix:versionable + - * (undefined) nofulltext noqueryorder + +[test:sameNameSibsFalseChildNodeDefinition] + - * (undefined) nofulltext noqueryorder + + * (nt:base) = test:sameNameSibsFalseChildNodeDefinition compute + +[test:setProperty] > mix:referenceable + - test:multiProperty (undefined) multiple nofulltext noqueryorder + - * (undefined) nofulltext noqueryorder + + * (nt:base) = test:setProperty + +[test:setPropertyAssumingType] + - test:multiProperty (undefined) multiple nofulltext noqueryorder + - test:singleProperty (undefined) nofulltext noqueryorder + + * (nt:base) = test:setPropertyAssumingType + +[test:versionable] > mix:versionable + - test:initializeOnParentVersionProp (string) initialize nofulltext noqueryorder + - test:copyOnParentVersionProp (string) nofulltext noqueryorder + - test:abortOnParentVersionProp (string) abort nofulltext noqueryorder + - test:ignoreOnParentVersionProp (string) ignore nofulltext noqueryorder + - test:computeOnParentVersionProp (string) compute nofulltext noqueryorder + - test:versionOnParentVersionProp (string) version nofulltext noqueryorder + - * (undefined) nofulltext noqueryorder + + test:computeOnParentVersion (nt:base) = nt:unstructured compute + + test:initializeOnParentVersion (nt:base) = nt:unstructured initialize + + test:versionOnParentVersion (nt:base) = nt:unstructured version + + * (nt:base) = test:versionable + + test:ignoreOnParentVersion (nt:base) = nt:unstructured ignore + + test:abortOnParentVersion (nt:base) = nt:unstructured abort + + test:copyOnParentVersion (nt:base) = nt:unstructured + +[test:orderableFolder] > nt:folder orderable + + * (nt:base) = test:orderableFolder + +[test:autoCreate] + - test:property (String) = 'default value' autocreated + - test:propertyMulti (String) = 'value1', 'value2' autocreated multiple + + test:folder (nt:folder) = nt:folder autocreated diff --git a/oak-jcr/src/test/resources/repositoryStubImpl.properties b/oak-jcr/src/test/resources/repositoryStubImpl.properties new file mode 100644 index 00000000000..b47cb859107 --- /dev/null +++ b/oak-jcr/src/test/resources/repositoryStubImpl.properties @@ -0,0 +1,533 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Stub implementation class +javax.jcr.tck.repository_stub_impl=org.apache.jackrabbit.oak.jcr.OakRepositoryStub + +# credential configuration +javax.jcr.tck.superuser.name=admin +javax.jcr.tck.superuser.pwd=admin +javax.jcr.tck.readwrite.name=admin +javax.jcr.tck.readwrite.pwd=admin +javax.jcr.tck.readonly.name=anonymous +javax.jcr.tck.readonly.pwd= + +# global test configuration +javax.jcr.tck.testroot=/testroot +javax.jcr.tck.nodetype=nt:unstructured +javax.jcr.tck.nodetypenochildren=nt:address +javax.jcr.tck.nodename1=node1 +javax.jcr.tck.nodename2=node2 +javax.jcr.tck.nodename3=node3 +javax.jcr.tck.nodename4=node4 +javax.jcr.tck.propertyname1=prop1 +javax.jcr.tck.propertyname2=prop2 +javax.jcr.tck.propertyvalue1=value1 +javax.jcr.tck.propertyvalue2=value2 +#javax.jcr.tck.propertytype1=String +#javax.jcr.tck.propertytype2=String +javax.jcr.tck.workspacename=default + +# namespace configuration +javax.jcr.tck.namespaces=test +javax.jcr.tck.namespaces.test=http://www.apache.org/jackrabbit/test + +# retention and hold +javax.jcr.tck.holdname=hold + +# repository factory class name +javax.jcr.tck.repository.factory=org.apache.jackrabbit.oak.jcr.OakRepositoryFactory + +# sample for per test case config overriding +# Test class: AddNodeText +# Test method: testName +javax.jcr.tck.AddNodeTest.testName.nodename1=myname + +# ============================================================================== +# JAVAX.JCR CONFIGURATION +# ============================================================================== + +# Test class: ItemDefTest +javax.jcr.tck.ItemDefTest.testroot=/testdata + +# Test class: ItemReadMethodsTest +javax.jcr.tck.ItemReadMethodsTest.testroot=/testdata + +# Test class: NodeReadMethodsTest +javax.jcr.tck.NodeReadMethodsTest.testroot=/testdata + +# Test class: PropertyTypeTest +javax.jcr.tck.PropertyTypeTest.testroot=/testdata + +# Test class: BinaryPropertyTest +javax.jcr.tck.BinaryPropertyTest.testroot=/testdata + +# Test class: BooleanPropertyTest +javax.jcr.tck.BooleanPropertyTest.testroot=/testdata + +# Test class: DatePropertyTest +javax.jcr.tck.DatePropertyTest.testroot=/testdata + +# Test class: DecimalPropertyTest +javax.jcr.tck.DecimalPropertyTest.testroot=/testdata + +# Test class: DoublePropertyTest +javax.jcr.tck.DoublePropertyTest.testroot=/testdata + +# Test class: LongPropertyTest +javax.jcr.tck.LongPropertyTest.testroot=/testdata + +# Test class: NamePropertyTest +javax.jcr.tck.NamePropertyTest.testroot=/testdata + +# Test class: PathPropertyTest +javax.jcr.tck.PathPropertyTest.testroot=/testdata + +# Test class: ReferencePropertyTest +javax.jcr.tck.ReferencePropertyTest.testroot=/testdata + +# Test class: StringPropertyTest +javax.jcr.tck.StringPropertyTest.testroot=/testdata + +# Test class: SetValueVersionExceptionTest +# nodetype2: nodetype with a reference property +javax.jcr.tck.SetValueVersionExceptionTest.nodetype2=nt:linkedFile +# propertyname3: name of the single value reference property +javax.jcr.tck.SetValueVersionExceptionTest.propertyname3=jcr:content + +# Test class: SetValueValueFormatExceptionTest +javax.jcr.tck.SetValueValueFormatExceptionTest.nodetype=test:canSetProperty +javax.jcr.tck.SetValueValueFormatExceptionTest.testValue.propertyname1=Boolean +javax.jcr.tck.SetValueValueFormatExceptionTest.testValueArray.propertyname1=BooleanMultiple +javax.jcr.tck.SetValueValueFormatExceptionTest.testString.propertyname1=Date +javax.jcr.tck.SetValueValueFormatExceptionTest.testStringArray.propertyname1=DateMultiple +javax.jcr.tck.SetValueValueFormatExceptionTest.testInputStream.propertyname1=Date +javax.jcr.tck.SetValueValueFormatExceptionTest.testLong.propertyname1=Boolean +javax.jcr.tck.SetValueValueFormatExceptionTest.testDouble.propertyname1=Boolean +javax.jcr.tck.SetValueValueFormatExceptionTest.testCalendar.propertyname1=Boolean +javax.jcr.tck.SetValueValueFormatExceptionTest.testBoolean.propertyname1=Date +javax.jcr.tck.SetValueValueFormatExceptionTest.testNode.propertyname1=Boolean +javax.jcr.tck.SetValueValueFormatExceptionTest.testNodeNotReferenceable.propertyname1=ReferenceConstraints + +# Test class: SetPropertyAssumeTypeTest +javax.jcr.tck.SetPropertyAssumeTypeTest.nodetype=test:canSetProperty +javax.jcr.tck.SetPropertyAssumeTypeTest.testStringConstraintViolationExceptionBecauseOfInvalidTypeParameter.propertyname1=String +javax.jcr.tck.SetPropertyAssumeTypeTest.testValueConstraintViolationExceptionBecauseOfInvalidTypeParameter.propertyname1=String +javax.jcr.tck.SetPropertyAssumeTypeTest.testValuesConstraintViolationExceptionBecauseOfInvalidTypeParameter.propertyname1=StringMultiple + +# Test class: UndefinedPropertyTest +javax.jcr.tck.UndefinedPropertyTest.testroot=/testdata + +# Test class: PropertyReadMethodsTest +javax.jcr.tck.PropertyReadMethodsTest.testroot=/testdata + +# Test class: NodeIteratorTest +javax.jcr.tck.NodeIteratorTest.testroot=/testdata + +# Test class: NodeDiscoveringNodeTypesTest +javax.jcr.tck.NodeDiscoveringNodeTypesTest.testroot=/testdata + +# Test class: RepositoryDescriptorTest +javax.jcr.tck.RepositoryDescriptorTest.testroot=/testdata + +# Test class: WorkspaceReadMethodsTest +javax.jcr.tck.WorkspaceReadMethodsTest.testroot=/testdata + +# Test class: SessionReadMethodsTest +javax.jcr.tck.SessionReadMethodsTest.testroot=/testdata + +# Test class: NamespaceRegistryReadMethodsTest +javax.jcr.tck.NamespaceRegistryReadMethodsTest.testroot=/testdata + +# Test class: NamespaceRemappingTest +javax.jcr.tck.NamespaceRemappingTest.testroot=/testdata + +# Test class: SessionTest +# Test method: testMoveItemExistsException +# nodetype that does not allow same name siblings +javax.jcr.tck.SessionTest.testMoveItemExistsException.nodetype2=nt:folder +# valid node type that can be added as child of nodetype2 +javax.jcr.tck.SessionTest.testMoveItemExistsException.nodetype3=nt:folder + +# Test class: SessionTest +# Test method: testSaveConstraintViolationException +# nodetype that has a property that is mandatory but not autocreated +javax.jcr.tck.SessionTest.testSaveConstraintViolationException.nodetype2=nt:file + +# Test class: SessionUUIDTest +# node type that has a property of type PropertyType.REFERENCE +javax.jcr.tck.SessionUUIDTest.nodetype=nt:unstructured +# name of the property that is of type PropertyType.REFERENCE +javax.jcr.tck.SessionUUIDTest.propertyname1=foobar +# nodetype that has nodetype mix:referenceable assigned +javax.jcr.tck.SessionUUIDTest.nodetype2=test:refTargetNode + +# Test class: SessionUUIDTest +# Test method: testSaveMovedRefNode +# name of the property that can be modified +javax.jcr.tck.SessionUUIDTest.testSaveMovedRefNode.propertyname1=foobar + +# Test class: NodeTest +# Test method: testAddNodeItemExistsException +# nodetype that does not allow same name siblings and allows child nodes of +# the same type +javax.jcr.tck.NodeTest.testAddNodeItemExistsException.nodetype=nt:folder + +# Test class: NodeTest +# Test method: testRemoveMandatoryNode +# nodetype that has a mandatory child node definition +javax.jcr.tck.NodeTest.testRemoveMandatoryNode.nodetype2=nt:file +# nodetype of the mandatory child +javax.jcr.tck.NodeTest.testRemoveMandatoryNode.nodetype3=nt:unstructured +# name of the mandatory node +javax.jcr.tck.NodeTest.testRemoveMandatoryNode.nodename3=jcr:content + +# Test class: NodeTest +# Test method: testSaveConstraintViolationException +# nodetype that has a property that is mandatory but not autocreated +javax.jcr.tck.NodeTest.testSaveConstraintViolationException.nodetype2=nt:file + +# Test class: NodeAddMixinTest +# Test method: testAddInheritedMixin +# the parent type should inherit a mixin type +javax.jcr.tck.NodeAddMixinTest.testAddInheritedMixin.nodetype=test:setProperty + +# Test class: NodeUUIDTest +# node type that has a property of type PropertyType.REFERENCE +javax.jcr.tck.NodeUUIDTest.nodetype=nt:unstructured +# name of the property that is of type PropertyType.REFERENCE +javax.jcr.tck.NodeUUIDTest.propertyname1=ref +# nodetype that has nodetype mix:referenceable assigned +javax.jcr.tck.NodeUUIDTest.nodetype2=test:refTargetNode + +# Test class: NodeUUIDTest +# Test method: testSaveMovedRefNode +# name of the property that can be modified +javax.jcr.tck.NodeUUIDTest.testSaveMovedRefNode.propertyname1=foobar +# nodetype that has nodetype mix:referenceable assigned + +# Test class: NodeOrderableChildNodesTest +# nodetype that supports orderable child nodes +javax.jcr.tck.NodeOrderableChildNodesTest.nodetype2=nt:unstructured +# valid node type that can be added as child of nodetype 2 +javax.jcr.tck.NodeOrderableChildNodesTest.nodetype3=nt:unstructured + +# Test class: NodeOrderableChildNodesTest +# Test method: testOrderBeforeUnsupportedRepositoryOperationException +# nodetype that does not allow ordering of child nodes +javax.jcr.tck.NodeOrderableChildNodesTest.testOrderBeforeUnsupportedRepositoryOperationException.nodetype2=nt:folder +# valid node type that can be added as child of nodetype 2 +javax.jcr.tck.NodeOrderableChildNodesTest.testOrderBeforeUnsupportedRepositoryOperationException.nodetype3=nt:folder + +# Test class: SetPropertyNodeTest +# nodetype which is referenceable +javax.jcr.tck.SetPropertyNodeTest.nodetype=test:setProperty + +# Test class: SetPropertyValueTest +# property that allows multiple values +javax.jcr.tck.SetPropertyValueTest.propertyname2=test:multiProperty +javax.jcr.tck.SetPropertyValueTest.nodetype=test:setProperty + +# Test class: SetPropertyStringTest +# property that allows multiple values +javax.jcr.tck.SetPropertyStringTest.propertyname2=test:multiProperty +javax.jcr.tck.SetPropertyStringTest.nodetype=test:setProperty + +# Test class: WorkspaceCloneSameNameSibsTest +javax.jcr.tck.WorkspaceCloneSameNameSibsTest.sameNameSibsFalseNodeType=test:sameNameSibsFalseChildNodeDefinition +javax.jcr.tck.WorkspaceCloneSameNameSibsTest.sameNameSibsTrueNodeType=nt:unstructured + +# Test class: WorkspaceCopyBetweenWorkspacesSameNameSibsTest +javax.jcr.tck.WorkspaceCopyBetweenWorkspacesSameNameSibsTest.sameNameSibsFalseNodeType=test:sameNameSibsFalseChildNodeDefinition +javax.jcr.tck.WorkspaceCopyBetweenWorkspacesSameNameSibsTest.sameNameSibsTrueNodeType=nt:unstructured + +# Test class: WorkspaceCopySameNameSibsTest +javax.jcr.tck.WorkspaceCopySameNameSibsTest.sameNameSibsFalseNodeType=test:sameNameSibsFalseChildNodeDefinition +javax.jcr.tck.WorkspaceCopySameNameSibsTest.sameNameSibsTrueNodeType=nt:unstructured + +# Test class: WorkspaceMoveSameNameSibsTest +javax.jcr.tck.WorkspaceMoveSameNameSibsTest.sameNameSibsFalseNodeType=test:sameNameSibsFalseChildNodeDefinition +javax.jcr.tck.WorkspaceMoveSameNameSibsTest.sameNameSibsTrueNodeType=nt:unstructured + +# Test class: RepositoryLoginTest +javax.jcr.tck.RepositoryLoginTest.testroot=/testdata + +# Test class: RootNodeTest +javax.jcr.tck.RootNodeTest.testroot=/testdata + +# Test class: ReferenceableRootNodesTest +javax.jcr.tck.ReferenceableRootNodesTest.testroot=/testdata + +# Test class: ExportDocViewTest +javax.jcr.tck.ExportDocViewTest.testroot=/testdata + +# ------------------------------------------------------------------------------ +# observation configuration +# ------------------------------------------------------------------------------ + +# Test class: AddEventListenerTest +# Test method: testNodeType +javax.jcr.tck.AddEventListenerTest.testNodeType.nodetype2=nt:folder + +# Configuration settings for the serialization. +# Note that the serialization test tries to use as many features of the repository +# as possible, but fails silently if a feature is not available. You have to +# specify all of the following configuration entries, even if your repository does +# not support the feature that is associated with them. + +# Root node for the example tree +javax.jcr.tck.SerializationTest.testroot=/testdata/serialization + +# Node type to use for the example tree. Specify a node type that allows complex trees and all property types if possible +javax.jcr.tck.SerializationTest.nodetype=nt:unstructured + +# Name of the nodes for source and target tree +javax.jcr.tck.SerializationTest.sourceFolderName=source +javax.jcr.tck.SerializationTest.targetFolderName=target +javax.jcr.tck.SerializationTest.rootNodeName=test + +# List the properties whose values may change during serialization/deserialization. For example, +# the UUID of a node is unique in the repository, so it will have to change when you re-import +# a tree at a different location. +javax.jcr.tck.SerializationTest.propertyValueMayChange= jcr:created jcr:uuid jcr:versionHistory jcr:baseVersion jcr:predecessors P_Reference + +# List all properties which are skipped during xml import according specification chapter 7.3.3 +javax.jcr.tck.SerializationTest.propertySkipped= + +# The name of the test node types. For easier diagnostics, the node types have names +# that tell you the kind of information they store +javax.jcr.tck.SerializationTest.nodeTypesTestNode=NodeTypes +javax.jcr.tck.SerializationTest.mixinTypeTestNode=MixinTypes +javax.jcr.tck.SerializationTest.propertyTypesTestNode=PropertyTypes +javax.jcr.tck.SerializationTest.sameNameChildrenTestNode=SameNameChildren +javax.jcr.tck.SerializationTest.multiValuePropertiesTestNode=MultiValueProperties +javax.jcr.tck.SerializationTest.referenceableNodeTestNode=ReferenceableNode +javax.jcr.tck.SerializationTest.orderChildrenTestNode=OrderChildren +javax.jcr.tck.SerializationTest.namespaceTestNode=Namespace + +# The name of the test property types. +javax.jcr.tck.SerializationTest.stringTestProperty=P_String +javax.jcr.tck.SerializationTest.binaryTestProperty=P_Binary +javax.jcr.tck.SerializationTest.dateTestProperty=P_Date +javax.jcr.tck.SerializationTest.longTestProperty=P_Long +javax.jcr.tck.SerializationTest.doubleTestProperty=P_Double +javax.jcr.tck.SerializationTest.booleanTestProperty=P_Boolean +javax.jcr.tck.SerializationTest.nameTestProperty=P_Name +javax.jcr.tck.SerializationTest.pathTestProperty=P_Path +javax.jcr.tck.SerializationTest.referenceTestProperty=P_Reference +javax.jcr.tck.SerializationTest.multiValueTestProperty=P_MultiValue + +# node type not allowing same name sibs +javax.jcr.tck.SerializationTest.sameNameSibsFalseChildNodeDefinition=test:sameNameSibsFalseChildNodeDefinition + +# Test method: testVersioningExceptionSessionFileChild +# specified nodetype must be versionable and allow child nodes of the same type. +javax.jcr.tck.SerializationTest.testVersioningExceptionSessionFileChild.nodetype=test:versionable + +# Test method: testVersioningExceptionSessionFileParent +# specified nodetype must be versionable and allow child nodes of the same type. +javax.jcr.tck.SerializationTest.testVersioningExceptionSessionFileParent.nodetype=test:versionable + +# Test method: testSessionImportXmlOverwriteException +# requires a node type that does not allow same name siblings +javax.jcr.tck.SerializationTest.testSessionImportXmlOverwriteException.nodetype=nt:folder + +# Test class: ExportSysViewTest +javax.jcr.tck.ExportSysViewTest.testroot=/testdata + +# ============================================================================== +# JAVAX.JCR.NODETYPE CONFIGURATION +# ============================================================================== + +javax.jcr.tck.nodetype.testroot=/testdata + +javax.jcr.tck.NodeTypeCreationTest.testroot=/testroot + +# ============================================================================== +# JAVAX.JCR.QUERY CONFIGURATION +# ============================================================================== + +# Test class: SaveTest +# Test method: testConstraintViolationException +# Specified node type must not allow child nodes. +javax.jcr.tck.SaveTest.testConstraintViolationException.nodetype=nt:query + +# Test class: XPathQueryLevel1Test +javax.jcr.tck.XPathQueryLevel1Test.testroot=/testdata/query + +# Test class: XPathDocOrderTest +javax.jcr.tck.XPathDocOrderTest.testroot=/testdata/query + +# Test class: XPathPosIndexTest +javax.jcr.tck.XPathPosIndexTest.testroot=/testdata/query + +# Test class: XPathOrderByTest +javax.jcr.tck.XPathOrderByTest.testroot=/testdata/query + +# Test class: XPathSyntaxTest +javax.jcr.tck.XPathSyntaxTest.testroot=/testdata/query + +# Test class: XPathJcrPathTest +javax.jcr.tck.XPathJcrPathTest.testroot=/testdata + +# Test class: SQLQueryLevel1Test +javax.jcr.tck.SQLQueryLevel1Test.testroot=/testdata/query + +# Test class: SQLSyntaxTest +javax.jcr.tck.SQLSyntaxTest.testroot=/testdata/query + +# Test class: SQLOrderByTest +javax.jcr.tck.SQLOrderByTest.testroot=/testdata/query + +# Test class: DerefQueryLevel1Test +javax.jcr.tck.DerefQueryLevel1Test.testroot=/testdata + +# Test class: GetLanguageTest +javax.jcr.tck.GetLanguageTest.testroot=/testdata + +# Test class: GetPersistentQueryPathLevel1Test +javax.jcr.tck.GetPersistentQueryPathLevel1Test.testroot=/testdata + +# Test class: GetPropertyNamesTest +javax.jcr.tck.GetPropertyNamesTest.testroot=/testdata + +# Test class: GetStatementTest +javax.jcr.tck.GetStatementTest.testroot=/testdata + +# Test class: GetSupportedQueryLanguagesTest +javax.jcr.tck.GetSupportedQueryLanguagesTest.testroot=/testdata + +# Test class: SQLJcrPathTest +javax.jcr.tck.SQLJcrPathTest.testroot=/testdata + +# Test class: SQLPathTest +javax.jcr.tck.SQLPathTest.testroot=/testdata + +# Test class: PredicatesTest +javax.jcr.tck.PredicatesTest.testroot=/testdata + +# Test class: SimpleSelectionTest +javax.jcr.tck.SimpleSelectionTest.testroot=/testdata + +# ============================================================================== +# JAVAX.JCR.VERSIONING CONFIGURATION +# ============================================================================== + +# nodetype that is versionable. if it is not, an attempt is made to create versionable nodes +# by adding a mix:versionable mixin-type. +# NOTE: javax.jcr.tck.nodetype must define a non-versionable nodetype! +javax.jcr.tck.version.versionableNodeType=test:versionable +javax.jcr.tck.version.simpleVersionableNodeType=nt:unstructured +javax.jcr.tck.version.propertyValue=aPropertyValue +javax.jcr.tck.version.destination=/testroot/versionableNodeName3 + +# testroot for the version package +# the test root must allow versionable and non-versionable nodes being created below +javax.jcr.tck.version.testroot=/testroot + +# 3 nodes (nodeName1, nodeName2, nodeName3 with nt=versionableNodeType / nt=nonVersionableNodeType will be cloned to 2nd workspace +# nodename1 > used to persistently create versionable node below testroot +# nodename2 > used to create second versionable node below testroot (used for restore/workspace.restore with uuid-conflict) +# nodename3 > used to persistently create non-versionable node below testroot +javax.jcr.tck.version.nodename1=versionableNodeName1 +javax.jcr.tck.version.nodename2=versionableNodeName2 +javax.jcr.tck.version.nodename3=nonVersionableNodeName1 + +# nodename 4: versionabel child-node of the first versionable node with nodeName1 and nodetype 'versionableNodeType' +# used for: +# + creation of a node in the 2nd workspace, that does not exist in the first workspace +# + creation of a node in the 2nd workspace, in order to test uuid-conflicts with Workspace.restore. +# + creation of a sub-node in the default workspace, in order to test uuid-conflicts with Node.restore. +# + NOTE: the nodetype with 'versionableNodeType' must define its children nodes to either have COPY or VERSION +# OPV behaviour in order to successfully test Node.restore and Workspace.restore with uuid conflict. +javax.jcr.tck.version.nodename4=childNodeName + +# path to existing String-properties and a new value for the property, that allows to test the indicated OPV behaviour +javax.jcr.tck.OnParentVersionAbortTest.propertyname1=test:abortOnParentVersionProp +javax.jcr.tck.OnParentVersionComputeTest.propertyname1=test:computeOnParentVersionProp +javax.jcr.tck.OnParentVersionCopyTest.propertyname1=test:copyOnParentVersionProp +javax.jcr.tck.OnParentVersionIgnoreTest.propertyname1=test:ignoreOnParentVersionProp +javax.jcr.tck.OnParentVersionInitializeTest.propertyname1=test:initializeOnParentVersionProp + +# Test class: RestoreTest +# Test method: testRestoreWithUUIDConflict +# nodename4 must be the name of a child node with a OPV definition COPY or VERSION +javax.jcr.tck.RestoreTest.testRestoreWithUUIDConflict.nodename4=test:versionOnParentVersion +javax.jcr.tck.RestoreTest.testRestoreLabel.nodename4=test:versionOnParentVersion +javax.jcr.tck.RestoreTest.testRestoreName.nodename4=test:versionOnParentVersion +javax.jcr.tck.RestoreTest.testRestoreNameJcr2.nodename4=test:versionOnParentVersion +javax.jcr.tck.RestoreTest.propertyValue1=version1 +javax.jcr.tck.RestoreTest.propertyValue2=version2 + +# Test class: WorkspaceRestoreTest +javax.jcr.tck.WorkspaceRestoreTest.testRestoreLabel.nodename4=test:versionOnParentVersion +javax.jcr.tck.WorkspaceRestoreTest.testRestoreName.nodename4=test:versionOnParentVersion + +# config for nodes that show the indicated OPV behaviour: +# nodes are added in order to test the versioning behaviour indicated by the test-class name. +# NOTE: +# - nodename4 is uses as name for the childnode +# - nodetype is used as nodetype name for the childnode +# - the specified child node is created below nodename1 with versionableNodeType +# the versionableNodeType and/or nodename1 may be overwritten with the individual +# testclass below. +javax.jcr.tck.OnParentVersionCopyTest.nodename4=test:copyOnParentVersion +javax.jcr.tck.OnParentVersionCopyTest.nodetype=nt:unstructured +javax.jcr.tck.OnParentVersionAbortTest.nodename4=test:abortOnParentVersion +javax.jcr.tck.OnParentVersionAbortTest.nodetype=nt:unstructured +javax.jcr.tck.OnParentVersionIgnoreTest.nodename4=test:ignoreOnParentVersion +javax.jcr.tck.OnParentVersionIgnoreTest.nodetype=nt:unstructured + +# ============================================================================== +# JAVAX.JCR.VERSIONING CONFIGURATION (simple versioning) +# ============================================================================== + +# nodetype that is versionable. if it is not, an attempt is made to create versionable nodes +# by adding a mix:versionable mixin-type. +# NOTE: javax.jcr.tck.nodetype must define a non-versionable nodetype! +javax.jcr.tck.simple.versionableNodeType=nt:unstructured +javax.jcr.tck.simple.propertyValue=aPropertyValue +javax.jcr.tck.simple.destination=/testroot/versionableNodeName3 + +# testroot for the version package +# the test root must allow versionable and non-versionable nodes being created below +javax.jcr.tck.simple.testroot=/testroot + +# 3 nodes (nodeName1, nodeName2, nodeName3 with nt=versionableNodeType / nt=nonVersionableNodeType will be cloned to 2nd workspace +# nodename1 > used to persistently create versionable node below testroot +# nodename2 > used to create second versionable node below testroot (used for restore/workspace.restore with uuid-conflict) +# nodename3 > used to persistently create non-versionable node below testroot +javax.jcr.tck.simple.nodename1=versionableNodeName1 +javax.jcr.tck.simple.nodename2=versionableNodeName2 +javax.jcr.tck.simple.nodename3=nonVersionableNodeName1 + +# nodename 4: versionabel child-node of the first versionable node with nodeName1 and nodetype 'versionableNodeType' +# used for: +# + creation of a node in the 2nd workspace, that does not exist in the first workspace +# + creation of a node in the 2nd workspace, in order to test uuid-conflicts with Workspace.restore. +# + creation of a sub-node in the default workspace, in order to test uuid-conflicts with Node.restore. +# + NOTE: the nodetype with 'versionableNodeType' must define its children nodes to either have COPY or VERSION +# OPV behaviour in order to successfully test Node.restore and Workspace.restore with uuid conflict. +javax.jcr.tck.simple.nodename4=childNodeName + +# ============================================================================== +# JAVAX.JCR.RETENTION CONFIGURATION +# ============================================================================== +javax.jcr.tck.retentionpolicyholder=/testconf/retentionTest + +# ============================================================================== +# LIFECYCLE MANAGEMENT CONFIGURATION +# ============================================================================== +javax.jcr.tck.lifecycleNode=/testdata/lifecycle/node diff --git a/oak-js/lib/oak.js b/oak-js/lib/oak.js new file mode 100644 index 00000000000..864cfd2971f --- /dev/null +++ b/oak-js/lib/oak.js @@ -0,0 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var oak = { + // TODO +}; diff --git a/oak-js/package.json b/oak-js/package.json new file mode 100644 index 00000000000..1528c8ce996 --- /dev/null +++ b/oak-js/package.json @@ -0,0 +1,20 @@ +{ + "author": "Apache Jackrabbit ", + "name": "oak", + "description": "JavaScript bindings for Oak", + "version": "0.3.0-SNAPSHOT", + "repository": { + "type": "svn", + "url": "https://svn.apache.org/repos/asf/jackrabbit/oak/trunk/oak-js/" + }, + "bugs": { + "url" : "https://issues.apache.org/jira/browse/OAK", + "email" : "oak-dev@jackrabbit.apache.org" + }, + "dependencies": {}, + "devDependencies": {}, + "optionalDependencies": {}, + "engines": { + "node": "*" + } +} diff --git a/oak-mk-api/pom.xml b/oak-mk-api/pom.xml new file mode 100644 index 00000000000..53a20dd2d21 --- /dev/null +++ b/oak-mk-api/pom.xml @@ -0,0 +1,93 @@ + + + + + + 4.0.0 + + + org.apache.jackrabbit + oak-parent + 0.6-SNAPSHOT + ../oak-parent/pom.xml + + + oak-mk-api + Oak MicroKernel API + bundle + + + + + org.apache.felix + maven-bundle-plugin + + + + org.apache.jackrabbit.mk.api + + + + + + org.apache.felix + maven-scr-plugin + + + + + + + + org.osgi + org.osgi.core + provided + + + org.osgi + org.osgi.compendium + provided + + + biz.aQute + bndlib + provided + + + org.apache.felix + org.apache.felix.scr.annotations + provided + + + + + com.google.code.findbugs + jsr305 + 2.0.0 + provided + + + + + junit + junit + test + + + + diff --git a/oak-mk-api/src/main/java/org/apache/jackrabbit/mk/api/MicroKernel.java b/oak-mk-api/src/main/java/org/apache/jackrabbit/mk/api/MicroKernel.java new file mode 100644 index 00000000000..78025515ffd --- /dev/null +++ b/oak-mk-api/src/main/java/org/apache/jackrabbit/mk/api/MicroKernel.java @@ -0,0 +1,504 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.api; + +import java.io.InputStream; + +/** + * The MicroKernel Design Goals and Principles: + *
      + *
    • manage huge trees of nodes and properties efficiently
    • + *
    • MVCC-based concurrency control + * (writers don't interfere with readers, snapshot isolation)
    • + *
    • GIT/SVN-inspired DAG-based versioning model
    • + *
    • highly scalable concurrent read & write operations
    • + *
    • session-less API (there's no concept of sessions; an implementation doesn't need to track/manage session state)
    • + *
    • easily portable to C
    • + *
    • easy to remote
    • + *
    • efficient support for large number of child nodes
    • + *
    • integrated API for efficiently storing/retrieving large binaries
    • + *
    • human-readable data serialization (JSON)
    • + *
    + *

    + * The MicroKernel Data Model: + *

    + *
      + *
    • simple JSON-inspired data model: just nodes and properties
    • + *
    • a node consists of an unordered set of name -> item mappings. each + * property and child node is uniquely named and a single name can only + * refer to a property or a child node, not both at the same time. + *
    • properties are represented as name/value pairs
    • + *
    • supported property types: string, number, boolean, array
    • + *
    • a property value is stored and used as an opaque, unparsed character sequence
    • + *
    + *

    + * The Retention Policy for Revisions: + *

    + * TODO specify retention policy for old revisions, i.e. minimal guaranteed retention period (OAK-114) + *

    + *

    + * The Retention Policy for Binaries: + *

    + *

    + * The MicroKernel implementation is free to remove binaries if both of the + * following conditions are met: + *

    + *
      + *
    • If the binary is not references as a property value of the + * format ":blobId:<blobId>" where <blobId> is the id returned by + * {@link #write(InputStream in)}. This includes simple property values such as + * {"bin": ":blobId:1234"} as well as array property values such as + * {"array": [":blobId:1234", ":blobId:5678"]}.
    • + *
    • If the binary was stored before the last retained revision (this is to + * keep temporary binaries, and binaries that are not yet referenced).
    • + *
    + */ +public interface MicroKernel { + + //---------------------------------------------------------< REVISION ops > + + /** + * Return the id of the current head revision. + * + * @return the id of the head revision + * @throws MicroKernelException if an error occurs + */ + String getHeadRevision() throws MicroKernelException; + + /** + * Returns a list of all currently available (historical) head revisions in + * chronological order since a specific point. Private branch + * revisions won't be included in the result. + *

    + * Format: + *

    +     * [
    +     *   {
    +     *     "id" : "<revisionId>",
    +     *     "ts" : <revisionTimestamp>,
    +     *     "msg" : "<commitMessage>"
    +     *   },
    +     *   ...
    +     * ]
    +     * 
    + * The {@code path} parameter allows to filter the revisions by path, i.e. + * only those revisions that affected the subtree rooted at {@code path} + * will be included. + *

    + * The {@code maxEntries} parameter allows to limit the number of revisions + * returned. if {@code maxEntries < 0} no limit will be applied. otherwise, + * if the number of revisions satisfying the specified {@code since} and + * {@code path} criteria exceeds {@code maxEntries}, only {@code maxEntries} + * entries will be returned (in chronological order, starting with the oldest). + * + * @param since timestamp (ms) of earliest revision to be returned + * @param maxEntries maximum #entries to be returned; + * if < 0, no limit will be applied. + * @param path optional path filter; if {@code null} or {@code ""} the + * default ({@code "/"}) will be assumed, i.e. no filter + * will be applied + * @return a list of revisions in chronological order in JSON format. + * @throws MicroKernelException if an error occurs + */ + String /* jsonArray */ getRevisionHistory(long since, int maxEntries, String path) + throws MicroKernelException; + + /** + * Waits for a commit to occur that is more recent than {@code oldHeadRevisionId}. + *

    + * This method allows for efficient polling for new revisions. The method + * will return the id of the current head revision if it is more recent than + * {@code oldHeadRevisionId}, or waits if either the specified amount of time + * has elapsed or a new head revision has become available. + *

    + * if a zero or negative {@code timeout} value has been specified the method + * will return immediately, i.e. calling {@code waitForCommit(0)} is + * equivalent to calling {@code getHeadRevision()}. + *

    + * Note that commits on a private branch will be ignored. + * + * @param oldHeadRevisionId id of earlier head revision + * @param timeout the maximum time to wait in milliseconds + * @return the id of the head revision + * @throws MicroKernelException if an error occurs + * @throws InterruptedException if the thread was interrupted + */ + String waitForCommit(String oldHeadRevisionId, long timeout) + throws MicroKernelException, InterruptedException; + + /** + * Returns a revision journal, starting with {@code fromRevisionId} + * and ending with {@code toRevisionId} in chronological order. + *

    + * Format: + *

    +     * [
    +     *   {
    +     *     "id" : "<revisionId>",
    +     *     "ts" : <revisionTimestamp>,
    +     *     "msg" : "<commitMessage>",
    +     *     "changes" : "<JSON diff>"
    +     *   },
    +     *   ...
    +     * ]
    +     * 
    + * If {@code fromRevisionId} and {@code toRevisionId} are not in chronological + * order the returned journal will be empty (i.e. {@code []}) + *

    + * The {@code path} parameter allows to filter the revisions by path, i.e. + * only those revisions that affected the subtree rooted at {@code path} + * will be included. The filter will also be applied to the JSON diff, i.e. + * the diff will include only those changes that affected the subtree rooted + * at {@code path}. + *

    + * A {@code MicroKernelException} is thrown if either {@code fromRevisionId} + * or {@code toRevisionId} doesn't exist, denotes a private branch + * revision or if another error occurs. + * + * @param fromRevisionId id of first revision to be returned in journal + * @param toRevisionId id of last revision to be returned in journal, + * if {@code null} the current head revision is assumed + * @param path optional path filter; if {@code null} or {@code ""} + * the default ({@code "/"}) will be assumed, i.e. no + * filter will be applied + * @return a chronological list of revisions in JSON format + * @throws MicroKernelException if any of the specified revisions doesn't exist or if another error occurs + */ + String /* jsonArray */ getJournal(String fromRevisionId, String toRevisionId, + String path) + throws MicroKernelException; + + /** + * Returns the JSON diff representation of the changes between the specified + * revisions. The changes will be consolidated if the specified range + * covers intermediary revisions. {@code fromRevisionId} and {@code toRevisionId} + * don't need not be in a specific chronological order. + *

    + * The {@code path} parameter allows to filter the changes included in the + * JSON diff, i.e. only those changes that affected the subtree rooted at + * {@code path} will be included. + *

    + * The {@code depth} limit applies to the subtree rooted at {@code path}. + * It allows to limit the depth of the diff, i.e. only changes up to the + * specified depth will be included in full detail. changes at paths exceeding + * the specified depth limit will be reported as {@code ^"/some/path" : {}}, + * indicating that there are unspecified changes below that path. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    {@code depth} valuescope of detailed diff
    -1no limit will be applied
    0changes affecting the properties and child node names of the node at {@code path}
    1changes affecting the properties and child node names of the node at {@code path} and its direct descendants
    ......
    + * + * @param fromRevisionId a revision id, if {@code null} the current head revision is assumed + * @param toRevisionId another revision id, if {@code null} the current head revision is assumed + * @param path optional path filter; if {@code null} or {@code ""} + * the default ({@code "/"}) will be assumed, i.e. no + * filter will be applied + * @param depth depth limit; if {@code -1} no limit will be applied + * @return JSON diff representation of the changes + * @throws MicroKernelException if any of the specified revisions doesn't exist or if another error occurs + */ + String /* JSON diff */ diff(String fromRevisionId, String toRevisionId, + String path, int depth) + throws MicroKernelException; + + //-------------------------------------------------------------< READ ops > + + /** + * Determines whether the specified node exists. + * + * @param path path denoting node + * @param revisionId revision id, if {@code null} the current head revision is assumed + * @return {@code true} if the specified node exists, otherwise {@code false} + * @throws MicroKernelException if the specified revision does not exist or if another error occurs + */ + boolean nodeExists(String path, String revisionId) throws MicroKernelException; + + /** + * Returns the number of child nodes of the specified node. + *

    + * This is a convenience method since this information could gathered by + * calling {@code getNodes(path, revisionId, 0, 0, 0, null)} and evaluating + * the {@code :childNodeCount} property. + * + * @param path path denoting node + * @param revisionId revision id, if {@code null} the current head revision is assumed + * @return the number of child nodes + * @throws MicroKernelException if the specified node or revision does not exist or if another error occurs + */ + long getChildNodeCount(String path, String revisionId) throws MicroKernelException; + + /** + * Returns the node tree rooted at the specified parent node with the + * specified depth, maximum child node maxChildNodes and offset. The depth of the + * returned tree is governed by the {@code depth} parameter: + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    depth = 0properties, including {@code :childNodeCount} and + * child node names (i.e. empty child node objects)
    depth = 1properties, child nodes and their properties (including + * {@code :childNodeCount}) and their child node names + * (i.e. empty child node objects)
    depth = 2[and so on...]
    + *

    + * Example (depth=0): + *

    +     * {
    +     *   "someprop" : "someval",
    +     *   ":childNodeCount" : 2,
    +     *   "child1" : {},
    +     *   "child2" : {}
    +     * }
    +     * 
    + * Example (depth=1): + *
    +     * {
    +     *   "someprop" : "someval",
    +     *   ":childNodeCount" : 2,
    +     *   "child1" : {
    +     *     "prop1" : 123,
    +     *     ":childNodeCount" : 2,
    +     *     "grandchild1" : {},
    +     *     "grandchild2" : {}
    +     *   },
    +     *   "child2" : {
    +     *     "prop1" : "bar",
    +     *     ":childNodeCount" : 0
    +     *   }
    +     * }
    +     * 
    + * Remarks: + *
      + *
    • If the property {@code :childNodeCount} equals 0, then the + * node does not have any child nodes. + *
    • If the value of {@code :childNodeCount} is larger than the number + * of returned child nodes, then the node has more child nodes than those + * included in the returned tree.
    • + *
    + * The {@code offset} parameter is only applied to the direct child nodes + * of the root of the returned node tree. {@code maxChildNodes} however + * is applied on all hierarchy levels. + *

    + * An {@code IllegalArgumentException} is thrown if both an {@code offset} + * greater than zero and a {@code filter} on node names (see below) have been + * specified. + *

    + * The order of the child nodes is stable for any given {@code revisionId}, + * i.e. calling {@code getNodes} repeatedly with the same {@code revisionId} + * is guaranteed to return the child nodes in the same order, but the + * specific order used is implementation-dependent and may change across + * different revisions of the same node. + *

    + * The optional {@code filter} parameter allows to specify glob patterns for names of + * nodes and/or properties to be included or excluded. + *

    + * Example: + *

    +     * {
    +     *   "nodes": [ "foo*", "-foo1" ],
    +     *   "properties": [ "*", "-:childNodeCount" ]
    +     * }
    +     * 
    + * In the above example all child nodes with names starting with "foo" will + * be included, except for nodes named "foo1"; similarly, all properties will + * be included except for the ":childNodeCount" metadata property (see below). + *

    + * Glob Syntax: + *

      + *
    • a {@code nodes} or {@code properties} filter consists of one or more globs.
    • + *
    • a glob prefixed by {@code -} (dash) is treated as an exclusion pattern; + * all others are considered inclusion patterns.
    • + *
    • a leading {@code -} (dash) must be escaped by prepending {@code \} (backslash) + * if it should be interpreted as a literal.
    • + *
    • {@code *} (asterisk) serves as a wildcard, i.e. it matches any + * substring in the target name.
    • + *
    • {@code *} (asterisk) occurrences within the glob to be interpreted as + * literals must be escaped by prepending {@code \} (backslash).
    • + *
    • a filter matches a target name if any of the inclusion patterns match but + * none of the exclusion patterns.
    • + *
    + * If no filter is specified the implicit default filter is assumed: + * {@code {"nodes":["*"],"properties":["*"]}} + *

    + * System-provided metadata properties: + *

      + *
    • {@code :childNodeCount} provides the actual number of direct child nodes; this property + * is included by the implicit default filter. it can be excluded by specifying a filter such + * as {@code {properties:["*", "-:childNodeCount"]}}
    • + *
    • {@code :hash} provides a content-based identifier for the subtree + * rooted at the {@code :hash} property's parent node. {@code :hash} values + * are similar to fingerprints. they can be compared to quickly determine + * if two subtrees are identical. if the {@code :hash} values are different + * the respective subtrees are different with regard to structure and/or properties. + * if on the other hand the {@code :hash} values are identical the respective + * subtrees are identical with regard to structure and properties. + * {@code :hash} is not included by the implicit default filter. + * it can be included by specifying a filter such as {@code {properties:["*", ":hash"]}} + *

      Returning the {@code :hash} property is optional. Some implementations + * might only return it on specific nodes or might not support it at all. + * If however a {@code :hash} property is returned it has to obey the contract + * described above.

    • + *
    + * + * @param path path denoting root of node tree to be retrieved + * @param revisionId revision id, if {@code null} the current head revision is assumed + * @param depth maximum depth of returned tree + * @param offset start position in the iteration order of child nodes (0 to start at the + * beginning) + * @param maxChildNodes maximum number of sibling child nodes to retrieve (-1 for all) + * @param filter optional filter on property and/or node names; if {@code null} or + * {@code ""} the default filter will be assumed + * @return node tree in JSON format or {@code null} if the specified node does not exist + * @throws MicroKernelException if the specified revision does not exist or if another error occurs + * @throws IllegalArgumentException if both an {@code offset > 0} and a {@code filter} on node names have been specified + */ + String /* jsonTree */ getNodes(String path, String revisionId, int depth, + long offset, int maxChildNodes, String filter) + throws MicroKernelException; + + //------------------------------------------------------------< WRITE ops > + + /** + * Applies the specified changes on the specified target node. + *

    + * If {@code path.length() == 0} the paths specified in the + * {@code jsonDiff} are expected to be absolute. + *

    + * The implementation tries to merge changes if the revision id of the + * commit is set accordingly. As an example, deleting a node is allowed if + * the node existed in the given revision, even if it was deleted in the + * meantime. + * + * @param path path denoting target node + * @param jsonDiff changes to be applied in JSON diff format. + * @param revisionId id of revision the changes are based on, + * if {@code null} the current head revision is assumed + * @param message commit message + * @return id of newly created revision + * @throws MicroKernelException if the specified revision doesn't exist or if another error occurs + */ + String /* revisionId */ commit(String path, String jsonDiff, + String revisionId, String message) + throws MicroKernelException; + + /** + * Creates a private branch revision off the specified public + * trunk revision. + *

    + * A {@code MicroKernelException} is thrown if {@code trunkRevisionId} doesn't + * exist, if it's not a trunk revision (i.e. it's not reachable + * by traversing the revision history in reverse chronological order starting + * from the current head revision) or if another error occurs. + * + * @param trunkRevisionId id of public trunk revision to base branch on, + * if {@code null} the current head revision is assumed + * @return id of newly created private branch revision + * @throws MicroKernelException if {@code trunkRevisionId} doesn't exist, + * if it's not a trunk revision + * or if another error occurs + * @see #merge(String, String) + */ + String /* revisionId */ branch(String trunkRevisionId) + throws MicroKernelException; + + /** + * Merges the specified private branch revision with the current + * head revision. + *

    + * A {@code MicroKernelException} is thrown if {@code branchRevisionId} doesn't + * exist, if it's not a branch revision, if the merge fails because of + * conflicting changes or if another error occurs. + * + * @param branchRevisionId id of private branch revision + * @param message commit message + * @return id of newly created head revision + * @throws MicroKernelException if {@code branchRevisionId} doesn't exist, + * if it's not a branch revision, if the merge + * fails because of conflicting changes or if + * another error occurs. + * @see #branch(String) + */ + String /* revisionId */ merge(String branchRevisionId, String message) + throws MicroKernelException; + + //--------------------------------------------------< BLOB READ/WRITE ops > + + /** + * Returns the length of the specified blob. + * + * @param blobId blob identifier + * @return length of the specified blob + * @throws MicroKernelException if the specified blob does not exist or if another error occurs + */ + long getLength(String blobId) throws MicroKernelException; + + /** + * Reads up to {@code length} bytes of data from the specified blob into + * the given array of bytes. An attempt is made to read as many as + * {@code length} bytes, but a smaller number may be read. + * The number of bytes actually read is returned as an integer. + * + * @param blobId blob identifier + * @param pos the offset within the blob + * @param buff the buffer into which the data is read. + * @param off the start offset in array {@code buff} + * at which the data is written. + * @param length the maximum number of bytes to read + * @return the total number of bytes read into the buffer, or + * {@code -1} if there is no more data because the end of + * the blob content has been reached. + * @throws MicroKernelException if the specified blob does not exist or if another error occurs + */ + int /* count */ read(String blobId, long pos, byte[] buff, int off, int length) + throws MicroKernelException; + + /** + * Stores the content of the given stream and returns an associated + * identifier for later retrieval. + *

    + * If identical stream content has been stored previously, then the existing + * identifier will be returned instead of storing a redundant copy. + *

    + * The stream is closed by this method. + * + * @param in InputStream providing the blob content + * @return blob identifier associated with the given content + * @throws MicroKernelException if an error occurs + */ + String /* blobId */ write(InputStream in) throws MicroKernelException; +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/api/MicroKernelException.java b/oak-mk-api/src/main/java/org/apache/jackrabbit/mk/api/MicroKernelException.java similarity index 95% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/api/MicroKernelException.java rename to oak-mk-api/src/main/java/org/apache/jackrabbit/mk/api/MicroKernelException.java index e4dd7d21a82..bd1bd63ed09 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/api/MicroKernelException.java +++ b/oak-mk-api/src/main/java/org/apache/jackrabbit/mk/api/MicroKernelException.java @@ -17,7 +17,7 @@ package org.apache.jackrabbit.mk.api; /** - * Exception thrown by methods of the MicroKernel API + * Exception thrown by methods of the {@code MicroKernel} API */ public class MicroKernelException extends RuntimeException { diff --git a/oak-mk-api/src/main/java/org/apache/jackrabbit/mk/api/package-info.java b/oak-mk-api/src/main/java/org/apache/jackrabbit/mk/api/package-info.java new file mode 100644 index 00000000000..c36655aa249 --- /dev/null +++ b/oak-mk-api/src/main/java/org/apache/jackrabbit/mk/api/package-info.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +@Version("0.1") +@Export(optional = "provide:=true") +package org.apache.jackrabbit.mk.api; + +import aQute.bnd.annotation.Export; +import aQute.bnd.annotation.Version; + diff --git a/oak-mk-perf/README b/oak-mk-perf/README new file mode 100644 index 00000000000..d3a37e837b3 --- /dev/null +++ b/oak-mk-perf/README @@ -0,0 +1,75 @@ +This module contains performance tests for microkernel instances. + +1. Usage + + The tests can be launched locally by calling directly the remote profile from the pom file using the following commands: + mvn clean test -Premote -Poakmk - for launching the tests against the oak microkernel or + mvn clean test -Premote -Pmongomk - for launching the tests against the mongodb microkernel. + + More than that the tests can be launched remotely, for example on an mongodb cluster.In this case the pom file uploads + the tests to the remote machine, runs them and collects the results.Use the following commands to remotely run the tests: + + mvn clean process-test-classes -Plocal -Pmongomk(or -Poakmk the microkernel used in tests) -Dremotehost= -Dpass= + +2. The test environment (mongodb cluster) + + For measuring the performance of the sharedcloud microkernel, I created a mongodb cluster in amazon cloud with the following components: + - 2 shards ( in the same time, replica sets) - each of it with 2 nodes installed on different platforms + - 3 configuration servers all of them installed on one platform + - 1 mongos instance + + The sharding is enabled and I'm using the following sharding key : {"path" :1, "revId":1}.I changed also the chunk size from 64 to 8 (MB). + + +3.Tests + + All the tests bellow were launched remotely on amazon cloud for both types of microkernel (oak and sharedcloud).The tests are all executed on the platform where the mongos instance is installed. + + MkAddNodesDifferentStructuresTest.testWriteNodesSameLevel + Creates 100000 nodes, all having the same parent node.All the nodes are added in a single microkernel commit. + oakmk 6.1 mongomk 11.4 + + MkAddNodesDifferentStructuresTest.testWriteNodes10Children + Creates 100000 nodes, in a pyramid tree structure.All of the nodes have 10 children.All the nodes are added in a single microkernel commit. + oakmk 3.6 mongomk 12.3 + MkAddNodesDifferentStructuresTest.testWriteNodes100Children + Creates 100000 nodes, in a pyramid tree structure.All of the nodes have 100 children.All the nodes are added in a single microkernel commit. + oakmk 2.8 mongomk 11.1 + + MkAddNodesDifferentStructuresTest.testWriteNodes1000Children + Creates 100000 nodes, in a pyramid tree structure.All of the nodes have 1000 children.All the nodes are added in a single microkernel commit. + oakmk 2.7 mongomk 11.9 + + MkAddNodesDifferentStructuresTest.testWriteNodes1Child + Creates 100 nodes, each node has only one child, so each of it is on a different tree level.All the nodes are added in a single microkernel commit. + oakmk 0.008 mongomk 0.5 + + MkAddNodesMultipleCommitsTest.testWriteNodesAllNodes1Commit + Create 10000 nodes, in a pyramid tree structure.All of the nodes have 100 children.Only one microkernel commit is performed for adding the nodes. + oakmk 0.14 mongomk 1.4 + + MkAddNodesMultipleCommitsTest.testWriteNodes50NodesPerCommit + Create 10000 nodes, in a pyramid tree structure.All of the nodes have 100 children.The nodes are added in chunks of 50 nodes per commit. + oakmk 0.41 mongomk Failure + + MkAddNodesMultipleCommitsTest.testWriteNodes1000NodesPerCommit + Create 10000 nodes, in a pyramid tree structure.All of the nodes have 100 children.The nodes are added in chunks of 1000 nodes per commit. + oakmk 0.2 mongomk 3.5 + + MkAddNodesMultipleCommitsTest.testWriteNodes1NodePerCommit + Create 10000 nodes, in a pyramid tree structure.All of the nodes have 100 children.Each node is individually added. + oakmk 8.3 mongomk Failure + + MKAddNodesRelativePathTest.testWriteNodesSameLevel + Create 1000 nodes, all on the same level.Each node is individually added (in a separate commit).Each node is added using the relative paths in the microkernel commit method. + oakmk 3 mongomk 150 + + MKAddNodesRelativePathTest.testWriteNodes10Children + Create 1000 nodes, each of them having exactly 10 children.Each node is individually added (in a separate commit).Each node is added using the relative paths in the microkernel commit method. + oakmk 4.2 mongomk Failure + + MKAddNodesRelativePathTest.testWriteNodes100Children + Create 1000 nodes, each of them having exactly 100 children.Each node is individually added (in a separate commit).Each node is added using the relative paths in the microkernel commit method. + oakmk 4.1 mongomk Failure + + diff --git a/oak-mk-perf/pom.xml b/oak-mk-perf/pom.xml new file mode 100644 index 00000000000..6d203ea3cb4 --- /dev/null +++ b/oak-mk-perf/pom.xml @@ -0,0 +1,160 @@ + + + + + 4.0.0 + + org.apache.jackrabbit + jackrabbit-oak + 0.6-SNAPSHOT + + oak-mk-perf + oak-mk-perf + http://maven.apache.org + + UTF-8 + + + + oakmk + + qe1 + oakmk + + + + mongomk + + qe + mongomk + + + + local + + + + + maven-dependency-plugin + + + ${project.build.outputDirectory} + + + + unpack-dependencies + process-resources + + unpack-dependencies + + + + + + + org.evolvis.maven.plugins.remote-testing + remote-testing-plugin + 0.6 + + ${remotehost} + ${user} + ${pass} + 0 + + /home/${user}/tests/ + ${basedir}/remotePom.xml + + + + remote testing + + clean + test + + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + org.apache.maven.surefire + surefire-junit47 + 2.12.3 + + + -Xmx2024m + + **/*Test.java + + + ${mktype} + + + + + + + + junit + junit + + + + org.apache.jackrabbit + oak-mongomk + ${project.version} + + + org.apache.jackrabbit + oak-mk + ${project.version} + + + com.h2database + h2 + 1.3.158 + + + com.cedarsoft.commons + test-utils + 5.0.9 + + + maven-cobertura-plugin + maven-plugins + + + maven-findbugs-plugin + maven-plugins + + + + + + + maven-repo.evolvis.org + http://maven-repo.evolvis.org/releases/ + + + + diff --git a/oak-mk-perf/remotePom.xml b/oak-mk-perf/remotePom.xml new file mode 100644 index 00000000000..a553e6d01a1 --- /dev/null +++ b/oak-mk-perf/remotePom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + org.apache.jackrabbit + remote-oak-mk-perf + 0.0.1-SNAPSHOT + jar + + remote-sharedcloud-oak-performance + http://maven.apache.org + + + UTF-8 + + + + remote + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.10 + + + org.apache.maven.surefire + surefire-junit47 + 2.12.3 + + + + + ${env.mktype} + + false + -Xmx4056m + + **/*Test.java + + + + + + + + diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/cluster/ClusterNode.java b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/tasks/GenericWriteTask.java similarity index 60% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/cluster/ClusterNode.java rename to oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/tasks/GenericWriteTask.java index 740ba15fbbf..1edb72bdd39 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/cluster/ClusterNode.java +++ b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/tasks/GenericWriteTask.java @@ -6,7 +6,7 @@ * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,27 +14,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.cluster; - -import java.net.InetSocketAddress; +package org.apache.jackrabbit.mk.tasks; import org.apache.jackrabbit.mk.api.MicroKernel; -import org.apache.jackrabbit.mk.client.Client; - -public class ClusterNode { +import org.apache.jackrabbit.mk.util.Committer; - private MicroKernel mk; - private String lastRevision; +public class GenericWriteTask implements Runnable { - public ClusterNode(MicroKernel mk) { + MicroKernel mk; + Committer committer; + String diff; + int nodesPerCommit; + + public GenericWriteTask(MicroKernel mk, String diff, int nodesPerCommit) { this.mk = mk; + this.diff = diff; + this.nodesPerCommit = nodesPerCommit; + committer = new Committer(); } - - public void join(InetSocketAddress addr) { - Client client = new Client(addr); - String headRevision = client.getHeadRevision(); - if (lastRevision == null) { - lastRevision = headRevision; - } + + @Override + public void run() { + committer.addNodes(mk, diff, nodesPerCommit); } } diff --git a/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/testing/ConcurrentMicroKernelTestBase.java b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/testing/ConcurrentMicroKernelTestBase.java new file mode 100644 index 00000000000..bdf292b0ca4 --- /dev/null +++ b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/testing/ConcurrentMicroKernelTestBase.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.mk.testing; + +import java.util.ArrayList; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.util.Chronometer; +import org.apache.jackrabbit.mk.util.Configuration; +import org.apache.jackrabbit.mk.util.MicroKernelConfigProvider; +import org.junit.Before; +import org.junit.BeforeClass; + +public class ConcurrentMicroKernelTestBase { + public static int mkNumber = 3; + public ArrayList mks; + public Chronometer chronometer; + static MicroKernelInitializer initializator; + static Configuration conf; + + + /** + * Loads the corresponding microkernel initialization class and the + * microkernel configuration.The method searches for the mk.type + * system property in order to initialize the proper microkernel.By default, + * the oak microkernel will be instantiated. + * + * @throws Exception + */ + @BeforeClass + public static void beforeSuite() throws Exception { + + String mktype = System.getProperty("mk.type"); + initializator = (mktype == null || mktype.equals("oakmk")) ? new OakMicroKernelInitializer() + : new MongoMicroKernelInitializer(); + System.out.println("Tests will run against ***" + + initializator.getType() + "***"); + conf = MicroKernelConfigProvider.readConfig(); + } + + /** + * Creates a microkernel collection with only one microkernel. + * + * @throws Exception + */ + @Before + public void beforeTest() throws Exception { + mks = new MicroKernelCollection(initializator, conf, mkNumber) + .getMicroKernels(); + chronometer = new Chronometer(); + } + +} diff --git a/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/testing/MicroKernelCollection.java b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/testing/MicroKernelCollection.java new file mode 100644 index 00000000000..4949c55c174 --- /dev/null +++ b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/testing/MicroKernelCollection.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.mk.testing; + +import java.util.ArrayList; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.util.Configuration; + +/** + * Represents a collection of microkernels. + * + * + * + */ +public class MicroKernelCollection { + ArrayList mks; + + /** + * Initialize a collection of microkernels.All microkernels have the same + * configuration. + * + * @param initializator + * The initialization class of a particular microkernel type. + * @param conf + * The microkernel configuration data. + * @throws Exception + */ + public MicroKernelCollection(MicroKernelInitializer initializator, + Configuration conf, int size) throws Exception { + mks = initializator.init(conf, size); + } + + /** + * Returns a microkernel collection. + * + * @return An array of initialized microkernels. + */ + public ArrayList getMicroKernels() { + return mks; + } +} diff --git a/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/testing/MicroKernelInitializer.java b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/testing/MicroKernelInitializer.java new file mode 100644 index 00000000000..70efa838e04 --- /dev/null +++ b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/testing/MicroKernelInitializer.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.mk.testing; + +import java.util.ArrayList; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.util.Configuration; + +/** + * Interface for microkernel initialization. + * + * + * + */ +public interface MicroKernelInitializer { + public ArrayList init(Configuration conf,int mksNumber) throws Exception; + public String getType(); +} diff --git a/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/testing/MicroKernelTestBase.java b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/testing/MicroKernelTestBase.java new file mode 100644 index 00000000000..04d7d0d0d1d --- /dev/null +++ b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/testing/MicroKernelTestBase.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.testing; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.util.Chronometer; +import org.apache.jackrabbit.mk.util.Configuration; +import org.apache.jackrabbit.mk.util.MicroKernelConfigProvider; +import org.junit.Before; +import org.junit.BeforeClass; + +/** + * The test base class for tests that are using only one microkernel instance. + * + * + * + */ +public class MicroKernelTestBase { + + static MicroKernelInitializer initializator; + public MicroKernel mk; + public static Configuration conf; + public Chronometer chronometer; + + /** + * Loads the corresponding microkernel initialization class and the + * microkernel configuration.The method searches for the mk.type + * system property in order to initialize the proper microkernel.By default, + * the oak microkernel will be instantiated. + * + * @throws Exception + */ + @BeforeClass + public static void beforeSuite() throws Exception { + + String mktype = System.getProperty("mk.type"); + initializator = (mktype == null || mktype.equals("oakmk")) ? new OakMicroKernelInitializer() + : new MongoMicroKernelInitializer(); + System.out.println("Tests will run against ***" + + initializator.getType() + "***"); + conf = MicroKernelConfigProvider.readConfig(); + } + + /** + * Creates a microkernel collection with only one microkernel. + * + * @throws Exception + */ + @Before + public void beforeTest() throws Exception { + + mk = (new MicroKernelCollection(initializator, conf, 1)) + .getMicroKernels().get(0); + chronometer = new Chronometer(); + } + +} diff --git a/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/testing/MongoMicroKernelInitializer.java b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/testing/MongoMicroKernelInitializer.java new file mode 100644 index 00000000000..a5450ba16a8 --- /dev/null +++ b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/testing/MongoMicroKernelInitializer.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.mk.testing; + +import java.util.ArrayList; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.blobs.BlobStore; +import org.apache.jackrabbit.mk.util.Configuration; +import org.apache.jackrabbit.mongomk.api.NodeStore; +import org.apache.jackrabbit.mongomk.impl.BlobStoreMongo; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.impl.MongoMicroKernel; +import org.apache.jackrabbit.mongomk.impl.NodeStoreMongo; + +import com.mongodb.BasicDBObjectBuilder; + +/** + * Creates a {@code MongoMicroKernel}.Initialize the mongo database for the + * tests. + */ +public class MongoMicroKernelInitializer implements MicroKernelInitializer { + + public ArrayList init(Configuration conf, int mksNumber) + throws Exception { + + ArrayList mks = new ArrayList(); + MongoConnection mongoConnection; + mongoConnection = new MongoConnection(conf.getHost(), + conf.getMongoPort(), conf.getMongoDatabase()); + + // initialize the database + // temporary workaround.Remove the sleep. + Thread.sleep(1000); + mongoConnection.initializeDB(true); + mongoConnection = new MongoConnection(conf.getHost(), + conf.getMongoPort(), "admin"); + // set the shard key + mongoConnection.getDB() + .command( + BasicDBObjectBuilder + .start("shardCollection", "test.nodes") + .push("key").add("path", 1).add("revId", 1) + .pop().get()); + + for (int i = 0; i < mksNumber; i++) { + mongoConnection = new MongoConnection(conf.getHost(), + conf.getMongoPort() , conf.getMongoDatabase()); + NodeStore nodeStore = new NodeStoreMongo(mongoConnection); + BlobStore blobStore = new BlobStoreMongo(mongoConnection); + mks.add(new MongoMicroKernel(nodeStore, blobStore)); + } + + return mks; + } + + public String getType() { + return "Mongo Microkernel implementation"; + } +} diff --git a/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/testing/OakMicroKernelInitializer.java b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/testing/OakMicroKernelInitializer.java new file mode 100644 index 00000000000..4b4c0cccc70 --- /dev/null +++ b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/testing/OakMicroKernelInitializer.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.mk.testing; + +import java.util.ArrayList; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +import org.apache.jackrabbit.mk.core.Repository; +import org.apache.jackrabbit.mk.util.Configuration; + +/** + * Initialize a {@code MicroKernelImpl}.A new {@code Repository} is created for + * each initialization. + */ +public class OakMicroKernelInitializer implements MicroKernelInitializer { + + + + @Override + public ArrayList init(Configuration conf, int mksNumber) + throws Exception { + ArrayList mks = new ArrayList(); + Repository rep = new Repository(conf.getStoragePath() + + System.currentTimeMillis()); + rep.init(); + for (int i = 0; i < mksNumber; i++) { + mks.add(new MicroKernelImpl(rep)); + } + return mks; + } + + public String getType() { + // TODO Auto-generated method stub + return "Oak Microkernel"; + } + + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/RevisionStore.java b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/util/BlobStoreFS.java similarity index 55% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/store/RevisionStore.java rename to oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/util/BlobStoreFS.java index a039bf88148..f314af36efa 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/RevisionStore.java +++ b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/util/BlobStoreFS.java @@ -14,25 +14,34 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.store; - -import org.apache.jackrabbit.mk.model.ChildNodeEntriesMap; -import org.apache.jackrabbit.mk.model.Id; -import org.apache.jackrabbit.mk.model.MutableCommit; -import org.apache.jackrabbit.mk.model.MutableNode; +package org.apache.jackrabbit.mk.util; +import java.io.File; import java.io.InputStream; -/** - * - */ -public interface RevisionStore extends RevisionProvider { - - Id /*id*/ putNode(MutableNode node) throws Exception; - Id /*id*/ putCommit(MutableCommit commit) throws Exception; - Id /*id*/ putCNEMap(ChildNodeEntriesMap map) throws Exception; - void setHeadCommitId(Id commitId) throws Exception; - void lockHead(); - void unlockHead(); - String /*id*/ putBlob(InputStream in) throws Exception; -} +import org.apache.jackrabbit.mk.blobs.BlobStore; + +public class BlobStoreFS implements BlobStore { + + public BlobStoreFS(String rootPath) { + File rootDir = new File(rootPath); + if (!rootDir.isDirectory()) { + rootDir.mkdirs(); + } + + } + + public long getBlobLength(String blobId) throws Exception { + return 0; + } + + public int readBlob(String blobId, long blobOffset, byte[] buffer, + int bufferOffset, int length) throws Exception { + return 0; + } + + public String writeBlob(InputStream is) throws Exception { + return null; + } + +} \ No newline at end of file diff --git a/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/util/Chronometer.java b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/util/Chronometer.java new file mode 100644 index 00000000000..fdd1af8de30 --- /dev/null +++ b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/util/Chronometer.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.mk.util; + +public final class Chronometer { + private long begin, end; + + public void start() { + begin = System.currentTimeMillis(); + } + + public void stop() { + end = System.currentTimeMillis(); + } + + public long getTime() { + return end - begin; + } + + public long getMilliseconds() { + return end - begin; + } + + public double getSeconds() { + return (end - begin) / 1000.0; + } + + public double getMinutes() { + return (end - begin) / 60000.0; + } + + public double getHours() { + return (end - begin) / 3600000.0; + } +} \ No newline at end of file diff --git a/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/util/Committer.java b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/util/Committer.java new file mode 100644 index 00000000000..569c1eb00b9 --- /dev/null +++ b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/util/Committer.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.util; + +import org.apache.jackrabbit.mk.api.MicroKernel; + +public class Committer { + + /** + * Add nodes to the repository. + * + * @param mk + * The microkernel that is performing the action. + * @param diff + * The diff that is commited.All the nodes must be define by + * their absolute path. + * @param nodesPerCommit + * Number of nodes per commit. + */ + public void addNodes(MicroKernel mk, String diff, int nodesPerCommit) { + + if (nodesPerCommit == 0) { + mk.commit("", diff.toString(), null, ""); + return; + } + String[] string = diff.split(System.getProperty("line.separator")); + int i = 0; + StringBuilder finalCommit = new StringBuilder(); + for (String line : string) { + finalCommit.append(line); + i++; + if (i == nodesPerCommit) { + mk.commit("", finalCommit.toString(), null, ""); + finalCommit.setLength(0); + i = 0; + } + } + if (finalCommit.length() > 0) + mk.commit("", finalCommit.toString(), null, ""); + } + + /** + * Add an empty node to repository. + * + * @param mk + * Microkernel that is performing the action. + * @param parentPath + * @param name + * Name of the node. + */ + public void addNode(MicroKernel mk, String parentPath, String name) { + mk.commit(parentPath, "+\"" + name + "\" : {} \n", null, ""); + } + + /** + * Recursively builds a pyramid tree structure.Each node is added in a + * separate commit. + * + * @param mk + * Microkernel used for adding nodes. + * @param startingPoint + * The path where the node will be added. + * @param index + * @param numberOfChildren + * Number of children per level. + * @param nodesNumber + * Total nodes number. + * @param nodePrefixName + * The node's name prefix.The complete node name is + * prefix+indexNumber. + **/ + public void addPyramidStructure(MicroKernel mk, String startingPoint, + int index, int numberOfChildren, long nodesNumber, + String nodePrefixName) { + // if all the nodes are on the same level + if (numberOfChildren == 0) { + for (long i = 0; i < nodesNumber; i++) { + addNode(mk, startingPoint, nodePrefixName + i); + // System.out.println("Created node " + i); + } + return; + } + if (index >= nodesNumber) + return; + addNode(mk, startingPoint, nodePrefixName + index); + for (int i = 1; i <= numberOfChildren; i++) { + if (!startingPoint.endsWith("/")) + startingPoint = startingPoint + "/"; + addPyramidStructure(mk, startingPoint + nodePrefixName + index, + index * numberOfChildren + i, numberOfChildren, + nodesNumber, nodePrefixName); + } + + } +} diff --git a/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/util/Configuration.java b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/util/Configuration.java new file mode 100644 index 00000000000..c141f579037 --- /dev/null +++ b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/util/Configuration.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.mk.util; + +import java.util.Properties; + +public class Configuration { + + private static final String MK_TYPE = "mk.type"; + private static final String HOST = "hostname"; + private static final String MONGO_PORT = "mongo.port"; + private static final String STORAGE_PATH = "storage.path"; + private static final String DATABASE = "mongo.database"; + + private final Properties properties; + + public Configuration(Properties properties) { + this.properties = properties; + } + + public String getMkType() { + return properties.getProperty(MK_TYPE); + } + + public String getHost() { + return properties.getProperty(HOST); + } + + public int getMongoPort() { + return Integer.parseInt(properties.getProperty(MONGO_PORT)); + } + + public String getStoragePath() { + return properties.getProperty(STORAGE_PATH); + } + + public String getMongoDatabase() { + + return properties.getProperty(DATABASE); + } +} diff --git a/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/util/MicroKernelConfigProvider.java b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/util/MicroKernelConfigProvider.java new file mode 100644 index 00000000000..92f20032dec --- /dev/null +++ b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/util/MicroKernelConfigProvider.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.util; + +import java.io.InputStream; +import java.util.Properties; + +public class MicroKernelConfigProvider { + + /** + * Read the mk configuration from file. + * + * @param resourcePath + * @return + * @throws Exception + */ + public static Configuration readConfig(String resourcePath) + throws Exception { + + InputStream is = MicroKernelConfigProvider.class + .getResourceAsStream(resourcePath); + + Properties properties = new Properties(); + properties.load(is); + is.close(); + return new Configuration(properties); + } + + /** + * Read the mk configuration from config.cfg. + * + * @param resourcePath + * @return + * @throws Exception + */ + public static Configuration readConfig() throws Exception { + + InputStream is = MicroKernelConfigProvider.class + .getResourceAsStream("/config.cfg"); + + Properties properties = new Properties(); + properties.load(is); + // System.out.println(properties.toString()); + is.close(); + return new Configuration(properties); + } + +} diff --git a/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/util/MicroKernelOperation.java b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/util/MicroKernelOperation.java new file mode 100644 index 00000000000..4f7f626da89 --- /dev/null +++ b/oak-mk-perf/src/main/java/org/apache/jackrabbit/mk/util/MicroKernelOperation.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.util; + + + +/** + * Useful methods for building node structure. + * + * + */ +public class MicroKernelOperation { + + /** + * Builds a diff representing a pyramid node structure. + * + * @param The + * path where the first node will be added. + * @param index + * @param numberOfChildren + * The number of children that each node must have. + * @param nodesNumber + * Total number of nodes. + * @param nodePrefixName + * The node name prefix. + * @param diff + * The string where the diff is builded.Put an empty string for + * creating a new structure. + * @return + */ + public static StringBuilder buildPyramidDiff(String startingPoint, + int index, int numberOfChildren, long nodesNumber, + String nodePrefixName, StringBuilder diff) { + if (numberOfChildren == 0) { + for (long i = 0; i < nodesNumber; i++) + diff.append(addNodeToDiff(startingPoint, nodePrefixName + i)); + return diff; + } + if (index >= nodesNumber) + return diff; + diff.append(addNodeToDiff(startingPoint, nodePrefixName + index)); + //System.out.println("Create node "+ index); + for (int i = 1; i <= numberOfChildren; i++) { + if (!startingPoint.endsWith("/")) + startingPoint = startingPoint + "/"; + buildPyramidDiff(startingPoint + nodePrefixName + index, index + * numberOfChildren + i, numberOfChildren, nodesNumber, + nodePrefixName, diff); + } + return diff; + } + + private static String addNodeToDiff(String startingPoint, String nodeName) { + if (!startingPoint.endsWith("/")) + startingPoint = startingPoint + "/"; + + return ("+\"" + startingPoint + nodeName + "\" : {\"key\":\"00000000000000000000\"} \n"); + } +} diff --git a/oak-mk-perf/src/main/resources/config.cfg b/oak-mk-perf/src/main/resources/config.cfg new file mode 100644 index 00000000000..d3aa4334bc3 --- /dev/null +++ b/oak-mk-perf/src/main/resources/config.cfg @@ -0,0 +1,22 @@ +######################################################################### +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## + +mk.type=oak +hostname=localhost +mongo.port=27017 +mongo.database=test +storage.path=target/mk-tck-repo \ No newline at end of file diff --git a/oak-mk-perf/src/test/java/org/apache/jackrabbit/mk/tests/MKAddNodesRelativePathTest.java b/oak-mk-perf/src/test/java/org/apache/jackrabbit/mk/tests/MKAddNodesRelativePathTest.java new file mode 100644 index 00000000000..c03881976ec --- /dev/null +++ b/oak-mk-perf/src/test/java/org/apache/jackrabbit/mk/tests/MKAddNodesRelativePathTest.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.mk.tests; + +import org.apache.jackrabbit.mk.util.Committer; +import org.apache.jackrabbit.mk.testing.MicroKernelTestBase; +import org.junit.Ignore; +import org.junit.Test; + +/** + * Measure the time needed for writing nodes in different tree structures.Each + * node is committed separately.Each node is also committed using the relative + * path of the parent node. + * + * + */ + +public class MKAddNodesRelativePathTest extends MicroKernelTestBase { + + static String nodeNamePrefix = "N"; + static int nodesNumber = 1000; + + @Test + public void testWriteNodesSameLevel() throws Exception { + Committer commiter = new Committer(); + chronometer.start(); + commiter.addPyramidStructure(mk, "/", 0, 0, nodesNumber, nodeNamePrefix); + chronometer.stop(); + System.out.println("Total time for testWriteNodesSameLevel is " + + chronometer.getSeconds()); + } + + @Test + public void testWriteNodes10Children() { + Committer commiter = new Committer(); + chronometer.start(); + + commiter.addPyramidStructure(mk, "/", 0, 10, nodesNumber, + nodeNamePrefix); + chronometer.stop(); + System.out.println("Total time for testWriteNodes10Children is " + + chronometer.getSeconds()); + } + + @Test + public void testWriteNodes100Children() { + Committer commiter = new Committer(); + chronometer.start(); + commiter.addPyramidStructure(mk, "/", 0, 100, nodesNumber, + nodeNamePrefix); + chronometer.stop(); + System.out.println("Total time for testWriteNodes100Children is " + + chronometer.getSeconds()); + } +} diff --git a/oak-mk-perf/src/test/java/org/apache/jackrabbit/mk/tests/MkAddNodesDifferentStructuresTest.java b/oak-mk-perf/src/test/java/org/apache/jackrabbit/mk/tests/MkAddNodesDifferentStructuresTest.java new file mode 100644 index 00000000000..e6c5838e2b2 --- /dev/null +++ b/oak-mk-perf/src/test/java/org/apache/jackrabbit/mk/tests/MkAddNodesDifferentStructuresTest.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.tests; + +import org.apache.jackrabbit.mk.util.MicroKernelOperation; +import org.apache.jackrabbit.mk.testing.MicroKernelTestBase; +import org.apache.jackrabbit.mk.util.Committer; +import org.junit.Ignore; +import org.junit.Test; + +/** + * Measure the time needed for writing nodes in different tree structures.All + * the nodes are added in a single commit. + * + * + */ +public class MkAddNodesDifferentStructuresTest extends MicroKernelTestBase { + + static long nodesNumber = 100; + static String nodeNamePrefix = "N"; + + /** + * Tree structure: + *

    + * rootNode (/) + *

    + * N0 N1... Nn-1 Nn + */ + @Test + public void testWriteNodesSameLevel() { + + String diff = MicroKernelOperation.buildPyramidDiff("/", 0, 0, + nodesNumber, nodeNamePrefix, new StringBuilder()).toString(); + Committer committer = new Committer(); + chronometer.start(); + committer.addNodes(mk, diff, 0); + chronometer.stop(); + System.out.println("Total time for testWriteNodesSameLevel is " + + chronometer.getSeconds()); + } + + /** + * Tree structure: + *

    + * rootNode (/) + *

    + * N0 + *

    + * N1 + *

    + * N2 + *

    + * N3 + */ + @Test + public void testWriteNodes1Child() { + int nodesNumber = 100; + + String diff = MicroKernelOperation.buildPyramidDiff("/", 0, 1, + nodesNumber, nodeNamePrefix, new StringBuilder()).toString(); + Committer committer = new Committer(); + chronometer.start(); + committer.addNodes(mk, diff, 0); + chronometer.stop(); + System.out.println("Total time for testWriteNodes1Child is " + + chronometer.getSeconds()); + } + + /** + * Tree structure: + *

    + * Number of nodes per level =10^(level). + *

    + * Each node has 10 children. + */ + @Test + public void testWriteNodes10Children() { + + String diff = MicroKernelOperation.buildPyramidDiff("/", 0, 10, + nodesNumber, nodeNamePrefix, new StringBuilder()).toString(); + Committer committer = new Committer(); + chronometer.start(); + committer.addNodes(mk, diff, 0); + chronometer.stop(); + System.out.println("Total time for testWriteNodes10Children is " + + chronometer.getSeconds()); + } + + /** + * Tree structure: + *

    + * Number of nodes per level =100^(level). + *

    + * Each node has 100 children. + */ + @Test + public void testWriteNodes100Children() { + + String diff = MicroKernelOperation.buildPyramidDiff("/", 0, 100, + nodesNumber, nodeNamePrefix, new StringBuilder()).toString(); + Committer committer = new Committer(); + chronometer.start(); + committer.addNodes(mk, diff, 0); + chronometer.stop(); + System.out.println("Total time for testWriteNodes100Children is " + + chronometer.getSeconds()); + } + + /** + * Tree structure: + *

    + * Number of nodes per level =1000^(level). + *

    + * Each node has 1000 children. + */ + @Test + public void testWriteNodes1000Children() { + String diff = MicroKernelOperation.buildPyramidDiff("/", 0, 1000, + nodesNumber, nodeNamePrefix, new StringBuilder()).toString(); + Committer committer = new Committer(); + chronometer.start(); + committer.addNodes(mk, diff, 0); + chronometer.stop(); + System.out.println("Total time for testWriteNodes1000Children is " + + chronometer.getSeconds()); + } +} diff --git a/oak-mk-perf/src/test/java/org/apache/jackrabbit/mk/tests/MkAddNodesMultipleCommitsTest.java b/oak-mk-perf/src/test/java/org/apache/jackrabbit/mk/tests/MkAddNodesMultipleCommitsTest.java new file mode 100644 index 00000000000..46066ed10b1 --- /dev/null +++ b/oak-mk-perf/src/test/java/org/apache/jackrabbit/mk/tests/MkAddNodesMultipleCommitsTest.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.tests; + +import org.apache.jackrabbit.mk.util.MicroKernelOperation; +import org.apache.jackrabbit.mk.testing.MicroKernelTestBase; +import org.apache.jackrabbit.mk.util.Committer; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +/** + * Measure the time needed for writing the same node structure in one or + * multiple commit steps. + *

    + * Tree structure: + *

    + * Number of nodes per level =100^(level). + *

    + * Each node has 100 children. + * + * + * + * + */ + +public class MkAddNodesMultipleCommitsTest extends MicroKernelTestBase { + + static String diff; + static int nodesNumber = 1000; + static String nodeNamePrefix = "N"; + + @BeforeClass + public static void prepareDiff() { + diff = MicroKernelOperation.buildPyramidDiff("/", 0, 10, nodesNumber, + nodeNamePrefix, new StringBuilder()).toString(); + } + + @Test + public void testWriteNodesAllNodes1Commit() { + + Committer commiter = new Committer(); + chronometer.start(); + commiter.addNodes(mk, diff, 0); + chronometer.stop(); + System.out.println("Total time for testWriteNodesAllNodes1Commit is " + + chronometer.getSeconds()); + } + + @Test + public void testWriteNodes1NodePerCommit() { + + Committer commiter = new Committer(); + chronometer.start(); + commiter.addNodes(mk, diff, 1); + chronometer.stop(); + System.out.println("Total time for testWriteNodes1NodePerCommit is " + + chronometer.getSeconds()); + } + + @Test + public void testWriteNodes50NodesPerCommit() { + + Committer commiter = new Committer(); + chronometer.start(); + commiter.addNodes(mk, diff, 50); + chronometer.stop(); + System.out.println("Total time for testWriteNodes50NodesPerCommit is " + + chronometer.getSeconds()); + } + + @Test + public void testWriteNodes1000NodesPerCommit() { + + Committer commiter = new Committer(); + chronometer.start(); + commiter.addNodes(mk, diff, 10); + chronometer.stop(); + System.out + .println("Total time for testWriteNodes1000NodesPerCommit is " + + chronometer.getSeconds()); + } + +} diff --git a/oak-mk-perf/src/test/java/org/apache/jackrabbit/mk/tests/MkConcurrentAddNodes1CommitTest.java b/oak-mk-perf/src/test/java/org/apache/jackrabbit/mk/tests/MkConcurrentAddNodes1CommitTest.java new file mode 100644 index 00000000000..c31a285a506 --- /dev/null +++ b/oak-mk-perf/src/test/java/org/apache/jackrabbit/mk/tests/MkConcurrentAddNodes1CommitTest.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.tests; + +import java.util.ArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.apache.jackrabbit.mk.tasks.GenericWriteTask; +import org.apache.jackrabbit.mk.testing.ConcurrentMicroKernelTestBase; +import org.apache.jackrabbit.mk.util.MicroKernelOperation; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; + +import com.cedarsoft.test.utils.CatchAllExceptionsRule; + +/** + * Test class for microkernel concurrent writing.All the nodes are added in a + * single commit. + */ + +public class MkConcurrentAddNodes1CommitTest extends ConcurrentMicroKernelTestBase { + + // nodes for each worker + int nodesNumber = 100; + + /** + @Rule + public CatchAllExceptionsRule catchAllExceptionsRule = new CatchAllExceptionsRule(); +**/ + @Test + public void testConcurentWritingFlatStructure() throws InterruptedException { + + ArrayList tasks = new ArrayList(); + String diff; + for (int i = 0; i < mkNumber; i++) { + diff = MicroKernelOperation.buildPyramidDiff("/", 0, 0, + nodesNumber, "N" + i + "N", new StringBuilder()).toString(); + tasks.add(new GenericWriteTask(mks.get(i), diff, 0)); + System.out.println("The diff size is " + diff.getBytes().length); + } + + ExecutorService threadExecutor = Executors.newFixedThreadPool(mkNumber); + chronometer.start(); + for (GenericWriteTask genericWriteTask : tasks) { + threadExecutor.execute(genericWriteTask); + } + threadExecutor.shutdown(); + threadExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + chronometer.stop(); + System.out.println("Total time for is " + chronometer.getSeconds()); + } + + @Test + public void testConcurentWritingPyramid1() throws InterruptedException { + + ArrayList tasks = new ArrayList(); + String diff; + for (int i = 0; i < mkNumber; i++) { + diff = MicroKernelOperation.buildPyramidDiff("/", 0, 10, + nodesNumber, "N" + i + "N", new StringBuilder()).toString(); + tasks.add(new GenericWriteTask(mks.get(i), diff, 0)); + System.out.println("The diff size is " + diff.getBytes().length); + } + + ExecutorService threadExecutor = Executors.newFixedThreadPool(mkNumber); + chronometer.start(); + for (GenericWriteTask genericWriteTask : tasks) { + threadExecutor.execute(genericWriteTask); + } + threadExecutor.shutdown(); + threadExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + chronometer.stop(); + System.out.println("Total time is " + chronometer.getSeconds()); + } + + @Test + public void testConcurentWritingPyramid2() throws InterruptedException { + + ArrayList tasks = new ArrayList(); + String diff; + for (int i = 0; i < mkNumber; i++) { + diff = MicroKernelOperation.buildPyramidDiff("/", 0, 10, + nodesNumber, "N" + i + "N", new StringBuilder()).toString(); + tasks.add(new GenericWriteTask(mks.get(i), diff, 0)); + System.out.println("The diff size is " + diff.getBytes().length); + } + + ExecutorService threadExecutor = Executors.newFixedThreadPool(mkNumber); + chronometer.start(); + for (GenericWriteTask genericWriteTask : tasks) { + threadExecutor.execute(genericWriteTask); + } + threadExecutor.shutdown(); + threadExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + chronometer.stop(); + System.out.println("Total time for is " + chronometer.getSeconds()); + } +} diff --git a/oak-mk-perf/src/test/java/org/apache/jackrabbit/mk/tests/MkConcurrentAddNodesMultipleCommitTest.java b/oak-mk-perf/src/test/java/org/apache/jackrabbit/mk/tests/MkConcurrentAddNodesMultipleCommitTest.java new file mode 100644 index 00000000000..4ea1395f4fc --- /dev/null +++ b/oak-mk-perf/src/test/java/org/apache/jackrabbit/mk/tests/MkConcurrentAddNodesMultipleCommitTest.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.tests; + +import java.util.ArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.apache.jackrabbit.mk.tasks.GenericWriteTask; +import org.apache.jackrabbit.mk.testing.ConcurrentMicroKernelTestBase; +import org.apache.jackrabbit.mk.util.MicroKernelOperation; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; + +import com.cedarsoft.test.utils.CatchAllExceptionsRule; + +/** + * Test class for microkernel concurrent writing.The microkernel is adding 1000 + * nodes per commit. + */ + +public class MkConcurrentAddNodesMultipleCommitTest extends + ConcurrentMicroKernelTestBase { + + // nodes for each worker + int nodesNumber = 100; + int numberOfNodesPerCommit = 10; + + /** + * @Rule public CatchAllExceptionsRule catchAllExceptionsRule = new + * CatchAllExceptionsRule(); + */ + @Test + public void testConcurentWritingFlatStructure() throws InterruptedException { + + int children = 0; + ArrayList tasks = new ArrayList(); + String diff; + for (int i = 0; i < mkNumber; i++) { + diff = MicroKernelOperation.buildPyramidDiff("/", 0, children, + nodesNumber, "N" + i + "N", new StringBuilder()).toString(); + tasks.add(new GenericWriteTask(mks.get(i), diff, + numberOfNodesPerCommit)); + System.out.println("The diff size is " + diff.getBytes().length); + } + + ExecutorService threadExecutor = Executors.newFixedThreadPool(mkNumber); + chronometer.start(); + for (GenericWriteTask genericWriteTask : tasks) { + threadExecutor.execute(genericWriteTask); + } + threadExecutor.shutdown(); + threadExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + chronometer.stop(); + System.out.println("Total time is " + chronometer.getSeconds()); + } + + @Test + public void testConcurentWritingPyramid1() throws InterruptedException { + int children = 15; + ArrayList tasks = new ArrayList(); + String diff; + for (int i = 0; i < mkNumber; i++) { + diff = MicroKernelOperation.buildPyramidDiff("/", 0, children, + nodesNumber, "N" + i + "N", new StringBuilder()).toString(); + tasks.add(new GenericWriteTask(mks.get(i), diff, + numberOfNodesPerCommit)); + System.out.println("The diff size is " + diff.getBytes().length); + } + + ExecutorService threadExecutor = Executors.newFixedThreadPool(mkNumber); + chronometer.start(); + for (GenericWriteTask genericWriteTask : tasks) { + threadExecutor.execute(genericWriteTask); + } + threadExecutor.shutdown(); + threadExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + chronometer.stop(); + System.out.println("Total time is " + chronometer.getSeconds()); + } + + @Test + public void testConcurentWritingPyramid2() throws InterruptedException { + int children = 2; + ArrayList tasks = new ArrayList(); + String diff; + for (int i = 0; i < mkNumber; i++) { + + diff = MicroKernelOperation.buildPyramidDiff("/", 0, children, + nodesNumber, "N" + i + "N", new StringBuilder()).toString(); + + tasks.add(new GenericWriteTask(mks.get(i), diff, + numberOfNodesPerCommit)); + + } + + ExecutorService threadExecutor = Executors.newFixedThreadPool(mkNumber); + chronometer.start(); + for (GenericWriteTask genericWriteTask : tasks) { + threadExecutor.execute(genericWriteTask); + } + threadExecutor.shutdown(); + threadExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + chronometer.stop(); + System.out.println("Total time for is " + chronometer.getSeconds()); + } +} diff --git a/oak-mk-remote/pom.xml b/oak-mk-remote/pom.xml new file mode 100644 index 00000000000..f4c5dad49e8 --- /dev/null +++ b/oak-mk-remote/pom.xml @@ -0,0 +1,114 @@ + + + + + + 4.0.0 + + + org.apache.jackrabbit + oak-parent + 0.6-SNAPSHOT + ../oak-parent/pom.xml + + + oak-mk-remote + Oak MicroKernel Remoting + bundle + + + + + org.apache.felix + maven-bundle-plugin + + + + org.apache.jackrabbit.mk.server, + org.apache.jackrabbit.mk.client + + + + + + org.apache.felix + maven-scr-plugin + + + + + + + + org.osgi + org.osgi.core + provided + + + org.osgi + org.osgi.compendium + provided + + + biz.aQute + bndlib + provided + + + org.apache.felix + org.apache.felix.scr.annotations + provided + + + + + org.apache.jackrabbit + oak-mk + ${project.version} + + + + + org.apache.jackrabbit + oak-commons + ${project.version} + + + + + com.google.code.findbugs + jsr305 + 2.0.0 + provided + + + + + com.googlecode.json-simple + json-simple + 1.1 + test + + + junit + junit + test + + + + diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/client/Client.java b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/client/Client.java similarity index 71% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/client/Client.java rename to oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/client/Client.java index 3c36c37daaa..5f9c3581cd3 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/client/Client.java +++ b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/client/Client.java @@ -19,7 +19,6 @@ import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; -import java.net.Socket; import java.net.URI; import java.net.URISyntaxException; import java.util.concurrent.atomic.AtomicBoolean; @@ -28,20 +27,16 @@ import org.apache.jackrabbit.mk.api.MicroKernel; import org.apache.jackrabbit.mk.api.MicroKernelException; -import org.apache.jackrabbit.mk.server.Server; import org.apache.jackrabbit.mk.util.IOUtils; /** - * Client exposing a MicroKernel interface, that "remotes" commands + * Client exposing a {@code MicroKernel} interface, that "remotes" commands * to a server. - *

    - * All public methods inside this class are completely synchronized because - * HttpExecutor is not thread-safe. */ public class Client implements MicroKernel { - private static final String MK_EXCEPTION_PREFIX = MicroKernelException.class.getName() + ":"; - + private static final String MK_EXCEPTION_PREFIX = MicroKernelException.class.getName() + ":"; + private final InetSocketAddress addr; private final SocketFactory socketFactory; @@ -49,47 +44,32 @@ public class Client implements MicroKernel { private final AtomicBoolean disposed = new AtomicBoolean(); private HttpExecutor executor; - + /** - * Create a new instance of this class, given a URL to connect to. + * Returns the socket address of the given URL. * - * @param url url - * @return micro kernel + * @param url URL + * @return socket address */ - public static MicroKernel createHttpClient(String url) { + private static InetSocketAddress getAddress(String url) { try { URI uri = new URI(url); - return new Client(new InetSocketAddress(uri.getHost(), uri.getPort())); + return new InetSocketAddress(uri.getHost(), uri.getPort()); } catch (URISyntaxException e) { - throw new IllegalArgumentException(e.getMessage()); + throw new IllegalArgumentException(e); } } + /** - * Create a new instance of this class, where every request goes through an HTTP bridge - * before being delivered to a given micro kernel implementation. + * Create a new instance of this class. * - * @param mk micro kernel - * @return bridged micro kernel + * @param url socket address */ - public static MicroKernel createHttpBridge(MicroKernel mk) { - final Server server = new Server(mk); - - try { - server.start(); - } catch (IOException e) { - throw new IllegalArgumentException(e.getMessage()); - } - - return new Client(server.getAddress()) { - @Override - public synchronized void dispose() { - super.dispose(); - server.stop(); - } - }; + public Client(String url) { + this(getAddress(url)); } - + /** * Create a new instance of this class. * @@ -108,17 +88,15 @@ public Client(InetSocketAddress addr, SocketFactory socketFactory) { this.addr = addr; this.socketFactory = socketFactory; } - - //-------------------------------------------------- implements MicroKernel - - public synchronized void dispose() { - if (!disposed.compareAndSet(false, true)) { - return; - } - IOUtils.closeQuietly(executor); + + public void dispose() { + // do nothing } - public synchronized String getHeadRevision() throws MicroKernelException { + //-------------------------------------------------- implements MicroKernel + + @Override + public String getHeadRevision() throws MicroKernelException { Request request = null; try { @@ -131,15 +109,17 @@ public synchronized String getHeadRevision() throws MicroKernelException { } } - public synchronized String getRevisions(long since, int maxEntries) + @Override + public String getRevisionHistory(long since, int maxEntries, String path) throws MicroKernelException { Request request = null; try { - request = createRequest("getRevisions"); + request = createRequest("getRevisionHistory"); request.addParameter("since", since); request.addParameter("max_entries", maxEntries); + request.addParameter("path", path); return request.getString(); } catch (IOException e) { throw toMicroKernelException(e); @@ -148,14 +128,15 @@ public synchronized String getRevisions(long since, int maxEntries) } } - public synchronized String waitForCommit(String oldHeadRevision, long maxWaitMillis) + @Override + public String waitForCommit(String oldHeadRevisionId, long maxWaitMillis) throws MicroKernelException, InterruptedException { Request request = null; try { request = createRequest("waitForCommit"); - request.addParameter("revision_id", oldHeadRevision); + request.addParameter("revision_id", oldHeadRevisionId); request.addParameter("max_wait_millis", maxWaitMillis); return request.getString(); } catch (IOException e) { @@ -165,7 +146,8 @@ public synchronized String waitForCommit(String oldHeadRevision, long maxWaitMil } } - public synchronized String getJournal(String fromRevisionId, String toRevisionId, String filter) + @Override + public String getJournal(String fromRevisionId, String toRevisionId, String path) throws MicroKernelException { Request request = null; @@ -174,7 +156,7 @@ public synchronized String getJournal(String fromRevisionId, String toRevisionId request = createRequest("getJournal"); request.addParameter("from_revision_id", fromRevisionId); request.addParameter("to_revision_id", toRevisionId); - request.addParameter("filter", filter); + request.addParameter("path", path); return request.getString(); } catch (IOException e) { throw toMicroKernelException(e); @@ -183,7 +165,8 @@ public synchronized String getJournal(String fromRevisionId, String toRevisionId } } - public synchronized String diff(String fromRevisionId, String toRevisionId, String filter) + @Override + public String diff(String fromRevisionId, String toRevisionId, String path, int depth) throws MicroKernelException { Request request = null; @@ -191,7 +174,8 @@ public synchronized String diff(String fromRevisionId, String toRevisionId, Stri request = createRequest("diff"); request.addParameter("from_revision_id", fromRevisionId); request.addParameter("to_revision_id", toRevisionId); - request.addParameter("filter", filter); + request.addParameter("path", path); + request.addParameter("depth", depth); return request.getString(); } catch (IOException e) { throw toMicroKernelException(e); @@ -200,7 +184,8 @@ public synchronized String diff(String fromRevisionId, String toRevisionId, Stri } } - public synchronized boolean nodeExists(String path, String revisionId) + @Override + public boolean nodeExists(String path, String revisionId) throws MicroKernelException { Request request = null; @@ -217,7 +202,8 @@ public synchronized boolean nodeExists(String path, String revisionId) } } - public synchronized long getChildNodeCount(String path, String revisionId) + @Override + public long getChildNodeCount(String path, String revisionId) throws MicroKernelException { Request request = null; @@ -234,13 +220,8 @@ public synchronized long getChildNodeCount(String path, String revisionId) } } - public synchronized String getNodes(String path, String revisionId) - throws MicroKernelException { - - return getNodes(path, revisionId, 1, 0, -1, null); - } - - public synchronized String getNodes(String path, String revisionId, int depth, + @Override + public String getNodes(String path, String revisionId, int depth, long offset, int count, String filter) throws MicroKernelException { Request request = null; @@ -251,9 +232,11 @@ public synchronized String getNodes(String path, String revisionId, int depth, request.addParameter("revision_id", revisionId); request.addParameter("depth", depth); request.addParameter("offset", offset); - request.addParameter("count", count); + request.addParameter("max_child_nodes", count); request.addParameter("filter", filter); - return request.getString(); + // OAK-48: MicroKernel.getNodes() should return null for not existing nodes instead of throwing an exception + String result = request.getString(); + return result.equals("null") ? null : result; } catch (IOException e) { throw toMicroKernelException(e); } finally { @@ -261,7 +244,8 @@ public synchronized String getNodes(String path, String revisionId, int depth, } } - public synchronized String commit(String path, String jsonDiff, String revisionId, + @Override + public String commit(String path, String jsonDiff, String revisionId, String message) throws MicroKernelException { Request request = null; @@ -280,7 +264,43 @@ public synchronized String commit(String path, String jsonDiff, String revisionI } } - public synchronized long getLength(String blobId) throws MicroKernelException { + @Override + public String branch(String trunkRevisionId) + throws MicroKernelException { + + Request request = null; + + try { + request = createRequest("branch"); + request.addParameter("trunk_revision_id", trunkRevisionId); + return request.getString(); + } catch (IOException e) { + throw toMicroKernelException(e); + } finally { + IOUtils.closeQuietly(request); + } + } + + @Override + public String merge(String branchRevisionId, String message) + throws MicroKernelException { + + Request request = null; + + try { + request = createRequest("merge"); + request.addParameter("branch_revision_id", branchRevisionId); + request.addParameter("message", message); + return request.getString(); + } catch (IOException e) { + throw toMicroKernelException(e); + } finally { + IOUtils.closeQuietly(request); + } + } + + @Override + public long getLength(String blobId) throws MicroKernelException { Request request = null; try { @@ -294,7 +314,8 @@ public synchronized long getLength(String blobId) throws MicroKernelException { } } - public synchronized int read(String blobId, long pos, byte[] buff, int off, int length) + @Override + public int read(String blobId, long pos, byte[] buff, int off, int length) throws MicroKernelException { Request request = null; @@ -312,7 +333,8 @@ public synchronized int read(String blobId, long pos, byte[] buff, int off, int } } - public synchronized String write(InputStream in) throws MicroKernelException { + @Override + public String write(InputStream in) throws MicroKernelException { Request request = null; try { @@ -323,6 +345,7 @@ public synchronized String write(InputStream in) throws MicroKernelException { throw toMicroKernelException(e); } finally { IOUtils.closeQuietly(request); + IOUtils.closeQuietly(in); } } @@ -350,23 +373,7 @@ private MicroKernelException toMicroKernelException(IOException e) { * @throws MicroKernelException if an exception occurs */ private Request createRequest(String command) throws IOException, MicroKernelException { - if (disposed.get()) { - throw new IllegalStateException("This instance has already been disposed"); - } - if (executor != null && !executor.isAlive()) { - IOUtils.closeQuietly(executor); - executor = null; - } - if (executor == null) { - executor = new HttpExecutor(createSocket()); - } - return new Request(executor, command); + return new Request(socketFactory, addr, command); } - private Socket createSocket() throws IOException { - if (addr == null) { - return socketFactory.createSocket(); - } - return socketFactory.createSocket(addr.getAddress(), addr.getPort()); - } } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/client/HttpExecutor.java b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/client/HttpExecutor.java similarity index 81% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/client/HttpExecutor.java rename to oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/client/HttpExecutor.java index 397f91f060a..f696fce9351 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/client/HttpExecutor.java +++ b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/client/HttpExecutor.java @@ -16,13 +16,19 @@ */ package org.apache.jackrabbit.mk.client; +import org.apache.jackrabbit.mk.remote.util.BoundedInputStream; +import org.apache.jackrabbit.mk.remote.util.ChunkedInputStream; +import org.apache.jackrabbit.mk.util.IOUtils; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.InetSocketAddress; import java.net.Socket; import java.net.URLEncoder; import java.security.SecureRandom; @@ -30,10 +36,7 @@ import java.util.Map; import java.util.Random; -import org.apache.jackrabbit.mk.util.BoundedInputStream; -import org.apache.jackrabbit.mk.util.ChunkedInputStream; -import org.apache.jackrabbit.mk.util.ChunkedOutputStream; -import org.apache.jackrabbit.mk.util.IOUtils; +import javax.net.SocketFactory; /** * Executes commands as HTTP requests. @@ -50,7 +53,8 @@ class HttpExecutor implements Closeable { private OutputStream socketOut; - private final ChunkedOutputStream bodyOut = new ChunkedOutputStream(null); +// private final ChunkedOutputStream bodyOut = new ChunkedOutputStream(null); + private final ByteArrayOutputStream bodyOut = new ByteArrayOutputStream(256); private final ChunkedInputStream bodyIn = new ChunkedInputStream(null); @@ -59,10 +63,18 @@ class HttpExecutor implements Closeable { /** * Create a new instance of this class. * - * @param socket socket + * @param socketFactory socket factory + * @param socketAddress server address + * @throws IOException if the server could not be contacted */ - public HttpExecutor(Socket socket) { - this.socket = socket; + public HttpExecutor(SocketFactory socketFactory, InetSocketAddress socketAddress) + throws IOException { + if (socketAddress != null) { + socket = socketFactory.createSocket( + socketAddress.getAddress(), socketAddress.getPort()); + } else { + socket = socketFactory.createSocket(); + } } /** @@ -83,24 +95,27 @@ public InputStream execute(String command, Map params, InputStre socketOut = new BufferedOutputStream(socket.getOutputStream()); } String contentType = "application/x-www-form-urlencoded"; + String boundary = null; if (in != null) { - contentType = "multipart/form-data"; + boundary = getBoundary(); + contentType = "multipart/form-data; boundary=" + boundary; } writeLine(String.format("POST /%s HTTP/1.1", command)); writeLine(String.format("Content-Type: %s", contentType)); - writeLine("Transfer-Encoding: chunked"); - writeLine(""); - - bodyOut.recycle(socketOut); - + if (in != null) { - String boundary = getBoundary(); + bodyOut.write("--".getBytes()); bodyOut.write(boundary.getBytes()); - bodyOut.write("\r\n\r\n".getBytes()); + bodyOut.write("\r\n".getBytes()); + bodyOut.write("Content-Disposition: form-data; name=\"file\"; filename=\"upload\"\r\n".getBytes()); + bodyOut.write("Content-Type: application/octet-stream\r\n".getBytes()); + bodyOut.write("\r\n".getBytes()); IOUtils.copy(in, bodyOut); bodyOut.write("\r\n".getBytes()); + bodyOut.write("--".getBytes()); bodyOut.write(boundary.getBytes()); + bodyOut.write("--".getBytes ()); } else { for (Map.Entry param : params.entrySet()) { String s = String.format("%s=%s&", @@ -110,7 +125,10 @@ public InputStream execute(String command, Map params, InputStre } } - bodyOut.close(); + byte[] data = bodyOut.toByteArray(); + writeLine(String.format("Content-Length: %d", data.length)); + writeLine(""); + socketOut.write(data); socketOut.flush(); // read response @@ -192,8 +210,8 @@ public InputStream execute(String command, Map params, InputStre /** * Return a flag indicating whether the executor is alive. - * - * @return true if it is alive; false otherwise + * + * @return {@code true} if it is alive; {@code false} otherwise */ public boolean isAlive() { return !connectionClosed && !socket.isClosed(); @@ -202,6 +220,7 @@ public boolean isAlive() { /** * Close this executor. */ + @Override public void close() { IOUtils.closeQuietly(socketOut); IOUtils.closeQuietly(socketIn); @@ -268,7 +287,7 @@ private static String getBoundary() { for (int i = 0; i < 16; i++) { b.append(BOUNDARY_CHARACTERS[random.nextInt(BOUNDARY_CHARACTERS.length)]); } - boundary = String.format("------ClientFormBoundary%s--", b.toString()); + boundary = String.format("----ClientFormBoundary%s", b.toString()); } return boundary; } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/client/Request.java b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/client/Request.java similarity index 67% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/client/Request.java rename to oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/client/Request.java index 676f440eeba..ddf87a43f97 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/client/Request.java +++ b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/client/Request.java @@ -20,19 +20,23 @@ import java.io.Closeable; import java.io.IOException; import java.io.InputStream; +import java.net.InetSocketAddress; import java.util.LinkedHashMap; import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; + +import javax.net.SocketFactory; import org.apache.jackrabbit.mk.util.IOUtils; /** - * Contains the details of a request to some remote MicroKernel + * Contains the details of a request to some remote {@code MicroKernel} * implementation. */ class Request implements Closeable { - - private HttpExecutor executor; + + private final SocketFactory socketFactory; + + private final InetSocketAddress socketAddress; private final String command; @@ -40,26 +44,26 @@ class Request implements Closeable { private InputStream in; - private InputStream resultIn; - - private final AtomicBoolean executed = new AtomicBoolean(); - /** * Create a new instance of this class. * - * @param executor executor + * @param socketFactory socket factory + * @param socketAddress server address * @param command command name */ - public Request(HttpExecutor executor, String command) { - this.executor = executor; + public Request( + SocketFactory socketFactory, InetSocketAddress socketAddress, + String command) { + this.socketFactory = socketFactory; + this.socketAddress = socketAddress; this.command = command; } - + /** * Add a string parameter. - * + * * @param name name - * @param value value, if null the call is ignored + * @param value value, if {@code null} the call is ignored */ public Request addParameter(String name, String value) { if (value != null) { @@ -70,8 +74,8 @@ public Request addParameter(String name, String value) { /** * Add an integer parameter, equivalent to - * addParameter(name, String.valueOf(value)). - * + * {@code addParameter(name, String.valueOf(value))}. + * * @param name name * @param value value */ @@ -82,8 +86,8 @@ public Request addParameter(String name, int value) { /** * Add a long parameter, equivalent to - * addParameter(name, String.valueOf(value)). - * + * {@code addParameter(name, String.valueOf(value))}. + * * @param name name * @param value value */ @@ -108,13 +112,22 @@ public Request addFileParameter(String name, InputStream in) { * * @throws IOException if an I/O error occurs */ - public void execute() throws IOException { - if (!executed.compareAndSet(false, true)) { - return; + private byte[] execute() throws IOException { + HttpExecutor executor = new HttpExecutor(socketFactory, socketAddress); + try { + InputStream stream = executor.execute(command, params, in); + try { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + IOUtils.copy(stream, buffer); + return buffer.toByteArray(); + } finally { + stream.close(); + } + } finally { + executor.close(); } - resultIn = executor.execute(command, params, in); } - + /** * Return a string from the result stream. Automatically executes * the request first. @@ -123,73 +136,62 @@ public void execute() throws IOException { * @throws IOException if an I/O error occurs */ public String getString() throws IOException { - execute(); - - return new String(toByteArray(resultIn), "8859_1"); + return new String(execute(), "8859_1"); } /** * Return a boolean from the result stream, equivalent to - * Boolean.parseBoolean(getString()). + * {@code Boolean.parseBoolean(getString())}. * Automatically executes the request first. - * + * * @return boolean * @throws IOException if an I/O error occurs */ public boolean getBoolean() throws IOException { - execute(); - return Boolean.parseBoolean(getString()); } /** * Return a long from the result stream, equivalent to - * Long.parseLong(getString()). + * {@code Long.parseLong(getString())}. * Automatically executes the request first. - * + * * @return boolean * @throws IOException if an I/O error occurs */ public long getLong() throws IOException { - execute(); - return Long.parseLong(getString()); } /** * Read bytes from the result stream. Automatically executes the * request first. - * + * * @param b buffer * @param off offset * @param len length - * @return number of bytes or -1 if no more bytes are available - * + * @return number of bytes or {@code -1} if no more bytes are available + * * @throws IOException if an I/O error occurs */ public int read(byte[] b, int off, int len) throws IOException { - execute(); - - int count = 0; - while (count < len) { - int n = resultIn.read(b, off + count, len - count); - if (n < 0) { - break; - } - count += n; + if (len == 0) { + return 0; + } + + byte[] bytes = execute(); + len = Math.min(bytes.length, len); + if (len == 0) { + return -1; } - return count == 0 && len != 0 ? -1 : count; + + System.arraycopy(bytes, 0, b, off, len); + return len; } - + + @Override public void close() { - IOUtils.closeQuietly(resultIn); - - executor = null; - } - - private static byte[] toByteArray(InputStream in) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(1024); - IOUtils.copy(in, out); - return out.toByteArray(); + // do nothing } + } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/BoundedInputStream.java b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/remote/util/BoundedInputStream.java similarity index 92% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/util/BoundedInputStream.java rename to oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/remote/util/BoundedInputStream.java index 3245a302bbd..6fd0c522f42 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/BoundedInputStream.java +++ b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/remote/util/BoundedInputStream.java @@ -14,15 +14,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.util; +package org.apache.jackrabbit.mk.remote.util; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; /** - * Implementation of an InputStream that is bounded by a limit - * and will return -1 on reads when this limit is exceeded. + * Implementation of an {@code InputStream} that is bounded by a limit + * and will return {@code -1} on reads when this limit is exceeded. */ public class BoundedInputStream extends FilterInputStream { @@ -89,4 +89,4 @@ public void close() throws IOException { in = null; } } -} \ No newline at end of file +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/ChunkedInputStream.java b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/remote/util/ChunkedInputStream.java similarity index 78% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/util/ChunkedInputStream.java rename to oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/remote/util/ChunkedInputStream.java index 6a70e29673f..7b8fd81b15a 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/ChunkedInputStream.java +++ b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/remote/util/ChunkedInputStream.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.util; +package org.apache.jackrabbit.mk.remote.util; import java.io.EOFException; import java.io.FilterInputStream; @@ -22,6 +22,8 @@ import java.io.InputStream; import java.util.Arrays; +import org.apache.jackrabbit.mk.util.IOUtils; + /** * Input stream that reads and decodes HTTP chunks, assuming that no chunk * exceeds 32768 bytes and that a chunk's length is represented by exactly 4 @@ -32,18 +34,13 @@ public class ChunkedInputStream extends FilterInputStream { /** * Maximum chunk size. */ - public static final int MAX_CHUNK_SIZE = 0x8000; + public static final int MAX_CHUNK_SIZE = 0x100000; /** * CR + LF combination. */ private static final byte[] CRLF = "\r\n".getBytes(); - /** - * Chunk prefix (length encoded as hexadecimal string). - */ - private final byte[] prefix = new byte[4]; - /** * Chunk data. */ @@ -68,6 +65,11 @@ public class ChunkedInputStream extends FilterInputStream { * Flag indicating whether the last chunk was read. */ private boolean lastChunk; + + /** + * Flag indicating whether there was an error decomposing a chunk. + */ + private boolean chunkError; /** * Create a new instance of this class. @@ -118,28 +120,61 @@ public int read(byte[] b, int off, int len) throws IOException { private void readChunk() throws IOException { offset = length = 0; - readFully(in, prefix); - length = parseInt(prefix); + length = readLength(in); if (length < 0 || length > MAX_CHUNK_SIZE) { + chunkError = true; String msg = "Chunk size smaller than 0 or bigger than " + MAX_CHUNK_SIZE; throw new IOException(msg); } + readFully(in, data, 0, length); readFully(in, suffix); if (!Arrays.equals(suffix, CRLF)) { + chunkError = true; String msg = "Missing carriage return/line feed combination."; throw new IOException(msg); } - readFully(in, data, 0, length); + if (length == 0) { + lastChunk = true; + } + } + + private int readLength(InputStream in) throws IOException { + int len = 0; + + for (int i = 0; i < 5; i++) { + int n, ch = in.read(); + if (ch == -1) { + break; + } + if (ch >= '0' && ch <= '9') { + n = (ch - '0'); + } else if (ch >= 'A' && ch <= 'F') { + n = (ch - 'A' + 10); + } else if (ch >= 'a' && ch <= 'f') { + n = (ch - 'a' + 10); + } else if (ch == '\r') { + ch = in.read(); + if (ch != '\n') { + chunkError = true; + String msg = "Missing carriage return/line feed combination."; + throw new IOException(msg); + } + return len; + } else { + chunkError = true; + String msg = String.format("Expected hexadecimal character, actual: %c", ch); + throw new IOException(msg); + } + len = len * 16 + n; + } readFully(in, suffix); if (!Arrays.equals(suffix, CRLF)) { + chunkError = true; String msg = "Missing carriage return/line feed combination."; throw new IOException(msg); } - - if (length == 0) { - lastChunk = true; - } + return len; } private static void readFully(InputStream in, byte[] b) throws IOException { @@ -155,37 +190,10 @@ private static void readFully(InputStream in, byte[] b, int off, int len) throws } } - /** - * Parse an integer that is given in its hexadecimal representation as - * a byte array. - * - * @param b byte array containing 4 ASCII characters - * @return parsed integer - */ - private static int parseInt(byte[] b) throws IOException { - int result = 0; - - for (int i = 0; i < 4; i++) { - int c = (int) b[i]; - result <<= 4; - if (c >= '0' && c <= '9') { - result += c - '0'; - } else if (c >= 'A' && c <= 'F') { - result += c - 'A' + 10; - } else if (c >= 'a' && c <= 'f') { - result += c - 'a' + 10; - } else { - String msg = "Not a hexadecimal character: " + c; - throw new IOException(msg); - } - } - return result; - } - /** * Recycle this input stream. * - * @param out new underlying input stream + * @param in new underlying input stream */ public void recycle(InputStream in) { this.in = in; @@ -204,7 +212,7 @@ public void recycle(InputStream in) { public void close() throws IOException { if (in != null) { try { - while (!lastChunk) { + while (!chunkError && !lastChunk) { readChunk(); } } finally { diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/ChunkedOutputStream.java b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/remote/util/ChunkedOutputStream.java similarity index 93% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/util/ChunkedOutputStream.java rename to oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/remote/util/ChunkedOutputStream.java index 78ac495ba1f..e77c5a5f0b8 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/ChunkedOutputStream.java +++ b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/remote/util/ChunkedOutputStream.java @@ -14,14 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.util; +package org.apache.jackrabbit.mk.remote.util; import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; -import static org.apache.jackrabbit.mk.util.ChunkedInputStream.MAX_CHUNK_SIZE; +import static org.apache.jackrabbit.mk.remote.util.ChunkedInputStream.MAX_CHUNK_SIZE; /** * Output stream that encodes and writes HTTP chunks. @@ -58,8 +58,8 @@ public class ChunkedOutputStream extends FilterOutputStream { * * @param out underlying output stream. * @param size internal buffer size - * @throws IllegalArgumentException if size is smaller than 1 - * or bigger than 65535 + * @throws IllegalArgumentException if {@code size} is smaller than 1 + * or bigger than {@code 65535} */ public ChunkedOutputStream(OutputStream out, int size) { super(out); @@ -169,7 +169,7 @@ public void recycle(OutputStream out) { * Close this output stream. Flush the contents of the internal buffer * and writes the last chunk to the underlying output stream. Sets * the internal reference to the underlying output stream to - * null. Does not close the underlying output stream. + * {@code null}. Does not close the underlying output stream. * * @see java.io.FilterOutputStream#close() */ diff --git a/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/remote/util/package-info.java b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/remote/util/package-info.java new file mode 100644 index 00000000000..d1328693ceb --- /dev/null +++ b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/remote/util/package-info.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +@Version("0.1") +@Export(optional = "provide:=true") +package org.apache.jackrabbit.mk.remote.util; + +import aQute.bnd.annotation.Export; +import aQute.bnd.annotation.Version; + diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/server/BoundaryInputStream.java b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/BoundaryInputStream.java similarity index 94% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/server/BoundaryInputStream.java rename to oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/BoundaryInputStream.java index b0549c422e2..c9b7c0a05cc 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/server/BoundaryInputStream.java +++ b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/BoundaryInputStream.java @@ -26,22 +26,22 @@ class BoundaryInputStream extends InputStream { private InputStream in; - + private final byte[] boundary; - + private final byte[] buf; - + private int offset; - + private int count; - + private int boundaryIndex; - + private boolean eos; /** * Create a new instance of this class. - * + * * @param in base input * @param boundary boundary */ @@ -51,7 +51,7 @@ public BoundaryInputStream(InputStream in, String boundary) { /** * Create a new instance of this class. - * + * * @param in base input * @param boundary boundary * @param size size of internal read-ahead buffer @@ -61,12 +61,13 @@ public BoundaryInputStream(InputStream in, String boundary, int size) { this.boundary = ("\r\n" + boundary).getBytes(); // Must be able to unread this many bytes - if (size < this.boundary.length + 1) { - size = this.boundary.length + 1; + if (size < this.boundary.length + 2) { + size = this.boundary.length + 2; } buf = new byte[size]; } + @Override public int read() throws IOException { if (eos) { return -1; @@ -103,12 +104,13 @@ private void fillBuffer() throws IOException { if (count < 0) { eos = true; } + count += offset; } private int copy(byte[] b, int off, int len) throws IOException { int i = 0, j = 0; - while (i < count && j < len) { + while (offset + i < count && j < len) { if (boundary[boundaryIndex] == buf[offset + i]) { boundaryIndex++; i++; @@ -122,7 +124,6 @@ private int copy(byte[] b, int off, int len) throws IOException { i -= boundaryIndex; if (i < 0) { offset += i; - count += (-i); i = 0; } boundaryIndex = 0; @@ -141,4 +142,4 @@ public void close() throws IOException { in = null; eos = true; } -} \ No newline at end of file +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/server/FileServlet.java b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/FileServlet.java similarity index 99% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/server/FileServlet.java rename to oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/FileServlet.java index cedcc59c9cd..12f1f15352e 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/server/FileServlet.java +++ b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/FileServlet.java @@ -33,6 +33,7 @@ class FileServlet implements Servlet { /** Just one instance, no need to make constructor public */ private FileServlet() {} + @Override public void service(Request request, Response response) throws IOException { String file = request.getFile(); if (file.endsWith("/")) { diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/server/HttpProcessor.java b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/HttpProcessor.java similarity index 87% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/server/HttpProcessor.java rename to oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/HttpProcessor.java index 9be7b9361a4..58c9547d3ac 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/server/HttpProcessor.java +++ b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/HttpProcessor.java @@ -16,6 +16,8 @@ */ package org.apache.jackrabbit.mk.server; +import org.apache.jackrabbit.mk.util.IOUtils; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; @@ -23,34 +25,32 @@ import java.io.OutputStream; import java.net.Socket; -import org.apache.jackrabbit.mk.util.IOUtils; - /** * Process all HTTP requests on a single socket. */ -class HttpProcessor { - - private static final int INITIAL_SO_TIMEOUT = 1000; - +public class HttpProcessor { + + private static final int INITIAL_SO_TIMEOUT = 10000; + private static final int DEFAULT_SO_TIMEOUT = 30000; - + private static final int MAX_KEEP_ALIVE_REQUESTS = 100; private final Socket socket; - + private final Servlet servlet; - + private InputStream socketIn; - + private OutputStream socketOut; - - private final Request request = new Request(); - private final Response response = new Response(); + private final Request request = new Request(); + + private final Response response = new Response(); /** * Create a new instance of this class. - * + * * @param socket socket * @param servlet servlet to invoke for incoming requests */ @@ -58,20 +58,20 @@ public HttpProcessor(Socket socket, Servlet servlet) { this.socket = socket; this.servlet = servlet; } - + /** * Process all requests on a single socket. - * + * * @throws IOException if an I/O error occurs */ public void process() throws IOException { try { socketIn = new BufferedInputStream(socket.getInputStream()); socketOut = new BufferedOutputStream(socket.getOutputStream()); - + socket.setSoTimeout(INITIAL_SO_TIMEOUT); - - for (int requestNum = 0; ; requestNum++) { + + for (int requestNum = 0;; requestNum++) { if (!process(requestNum)) { break; } @@ -85,14 +85,14 @@ public void process() throws IOException { IOUtils.closeQuietly(socket); } } - + /** * Process a single request. - * + * * @param requestNum number of this request on the same persistent connection - * @return true if the connection should be kept alive; - * false otherwise - * + * @return {@code true} if the connection should be kept alive; + * {@code false} otherwise + * * @throws IOException if an I/O error occurs */ private boolean process(int requestNum) throws IOException { diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/server/MicroKernelServlet.java b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/MicroKernelServlet.java similarity index 78% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/server/MicroKernelServlet.java rename to oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/MicroKernelServlet.java index 5cebb4e5e69..6eeeaad0b9b 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/server/MicroKernelServlet.java +++ b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/MicroKernelServlet.java @@ -30,16 +30,16 @@ import org.apache.jackrabbit.mk.util.IOUtils; /** - * Servlet handling requests directed at a MicroKernel instance. + * Servlet handling requests directed at a {@code MicroKernel} instance. */ class MicroKernelServlet { - + /** The one and only instance of this servlet. */ public static MicroKernelServlet INSTANCE = new MicroKernelServlet(); - + /** Just one instance, no need to make constructor public */ private MicroKernelServlet() {} - + public void service(MicroKernel mk, Request request, Response response) throws IOException { String file = request.getFile(); int dotIndex = file.indexOf('.'); @@ -63,18 +63,18 @@ public void service(MicroKernel mk, Request request, Response response) throws I } response.setStatusCode(404); } - + private static interface Command { - + void execute(MicroKernel mk, Request request, Response response) throws IOException, MicroKernelException; } - + private static final Map COMMANDS = new HashMap(); static { COMMANDS.put("getHeadRevision", new GetHeadRevision()); - COMMANDS.put("getRevisions", new GetRevisions()); + COMMANDS.put("getRevisionHistory", new GetRevisionHistory()); COMMANDS.put("waitForCommit", new WaitForCommit()); COMMANDS.put("getJournal", new GetJournal()); COMMANDS.put("diff", new Diff()); @@ -82,46 +82,52 @@ void execute(MicroKernel mk, Request request, Response response) COMMANDS.put("getChildNodeCount", new GetChildNodeCount()); COMMANDS.put("getNodes", new GetNodes()); COMMANDS.put("commit", new Commit()); + COMMANDS.put("branch", new Branch()); + COMMANDS.put("merge", new Merge()); COMMANDS.put("getLength", new GetLength()); COMMANDS.put("read", new Read()); COMMANDS.put("write", new Write()); } - + static class GetHeadRevision implements Command { - + + @Override public void execute(MicroKernel mk, Request request, Response response) throws IOException, MicroKernelException { response.setContentType("text/plain"); response.write(mk.getHeadRevision()); - } + } } - static class GetRevisions implements Command { - + static class GetRevisionHistory implements Command { + + @Override public void execute(MicroKernel mk, Request request, Response response) throws IOException, MicroKernelException { - + long since = request.getParameter("since", 0L); int maxEntries = request.getParameter("max_entries", 10); + String path = request.getParameter("path", ""); response.setContentType("application/json"); - String json = mk.getRevisions(since, maxEntries); - if (request.getHeaders().containsKey("User-Agent")) { + String json = mk.getRevisionHistory(since, maxEntries, path); + if (request.getUserAgent() != null) { json = JsopBuilder.prettyPrint(json); } response.write(json); - } + } } - + static class WaitForCommit implements Command { + @Override public void execute(MicroKernel mk, Request request, Response response) throws IOException, MicroKernelException { String headRevision = mk.getHeadRevision(); - String oldHead = request.getParameter("old_revision_id", headRevision); + String oldHead = request.getParameter("revision_id", headRevision); long maxWaitMillis = request.getParameter("max_wait_millis", 0L); String currentHead; @@ -139,26 +145,28 @@ public void execute(MicroKernel mk, Request request, Response response) static class GetJournal implements Command { + @Override public void execute(MicroKernel mk, Request request, Response response) throws IOException, MicroKernelException { - + String headRevision = mk.getHeadRevision(); - + String fromRevisionId = request.getParameter("from_revision_id", headRevision); String toRevisionId = request.getParameter("to_revision_id", headRevision); - String filter = request.getParameter("filter", ""); + String path = request.getParameter("path", ""); response.setContentType("application/json"); - String json = mk.getJournal(fromRevisionId, toRevisionId, filter); - if (request.getHeaders().containsKey("User-Agent")) { + String json = mk.getJournal(fromRevisionId, toRevisionId, path); + if (request.getUserAgent() != null) { json = JsopBuilder.prettyPrint(json); } response.write(json); - } + } } static class Diff implements Command { + @Override public void execute(MicroKernel mk, Request request, Response response) throws IOException, MicroKernelException { @@ -166,11 +174,12 @@ public void execute(MicroKernel mk, Request request, Response response) String fromRevisionId = request.getParameter("from_revision_id", headRevision); String toRevisionId = request.getParameter("to_revision_id", headRevision); - String filter = request.getParameter("filter", ""); + String path = request.getParameter("path", ""); + int depth = request.getParameter("depth", 1); - response.setContentType("application/json"); - String json = mk.diff(fromRevisionId, toRevisionId, filter); - if (request.getHeaders().containsKey("User-Agent")) { + response.setContentType("text/plain"); + String json = mk.diff(fromRevisionId, toRevisionId, path, depth); + if (request.getUserAgent() != null) { json = JsopBuilder.prettyPrint(json); } response.write(json); @@ -179,6 +188,7 @@ public void execute(MicroKernel mk, Request request, Response response) static class NodeExists implements Command { + @Override public void execute(MicroKernel mk, Request request, Response response) throws IOException, MicroKernelException { @@ -194,6 +204,7 @@ public void execute(MicroKernel mk, Request request, Response response) static class GetChildNodeCount implements Command { + @Override public void execute(MicroKernel mk, Request request, Response response) throws IOException, MicroKernelException { @@ -209,48 +220,88 @@ public void execute(MicroKernel mk, Request request, Response response) static class GetNodes implements Command { + @Override public void execute(MicroKernel mk, Request request, Response response) throws IOException, MicroKernelException { - + String headRevision = mk.getHeadRevision(); String path = request.getParameter("path", "/"); String revisionId = request.getParameter("revision_id", headRevision); int depth = request.getParameter("depth", 1); long offset = request.getParameter("offset", 0L); - int count = request.getParameter("count", -1); + int maxChildNodes = request.getParameter("max_child_nodes", -1); String filter = request.getParameter("filter", ""); response.setContentType("application/json"); - String json = mk.getNodes(path, revisionId, depth, offset, count, filter); - if (request.getHeaders().containsKey("User-Agent")) { + String json = mk.getNodes(path, revisionId, depth, offset, maxChildNodes, filter); + // OAK-48: MicroKernel.getNodes() should return null for not existing nodes instead of throwing an exception + if (json == null) { + json = "null"; + } + if (request.getUserAgent() != null) { json = JsopBuilder.prettyPrint(json); } response.write(json); - } + } } static class Commit implements Command { + @Override public void execute(MicroKernel mk, Request request, Response response) throws IOException, MicroKernelException { - + String headRevision = mk.getHeadRevision(); String path = request.getParameter("path", "/"); String jsonDiff = request.getParameter("json_diff"); String revisionId = request.getParameter("revision_id", headRevision); String message = request.getParameter("message"); - + String newRevision = mk.commit(path, jsonDiff, revisionId, message); response.setContentType("text/plain"); response.write(newRevision); - } + } + } + + static class Branch implements Command { + + @Override + public void execute(MicroKernel mk, Request request, Response response) + throws IOException, MicroKernelException { + + String headRevision = mk.getHeadRevision(); + + String trunkRevisionId = request.getParameter("trunk_revision_id", headRevision); + + String newRevision = mk.branch(trunkRevisionId); + + response.setContentType("text/plain"); + response.write(newRevision); + } + } + + static class Merge implements Command { + + @Override + public void execute(MicroKernel mk, Request request, Response response) + throws IOException, MicroKernelException { + + String branchRevisionId = request.getParameter("branch_revision_id"); + String message = request.getParameter("message"); + + String newRevision = mk.merge(branchRevisionId, message); + + response.setContentType("text/plain"); + response.write(newRevision); + } } static class GetLength implements Command { + @Override public void execute(MicroKernel mk, Request request, Response response) throws IOException, MicroKernelException { @@ -264,12 +315,18 @@ public void execute(MicroKernel mk, Request request, Response response) static class Read implements Command { + @Override public void execute(MicroKernel mk, Request request, Response response) throws IOException, MicroKernelException { String blobId = request.getParameter("blob_id", ""); long pos = request.getParameter("pos", 0L); - int length = request.getParameter("length", -1); + int length = request.getParameter("length", -1); + + if (request.getUserAgent() == null) { + // let browsers guess the correct file format + response.setContentType("application/octet-stream"); + } OutputStream out = response.getOutputStream(); if (pos == 0L && length == -1) { @@ -289,6 +346,7 @@ public void execute(MicroKernel mk, Request request, Response response) static class Write implements Command { + @Override public void execute(MicroKernel mk, Request request, Response response) throws IOException, MicroKernelException { @@ -298,6 +356,6 @@ public void execute(MicroKernel mk, Request request, Response response) response.setContentType("text/plain"); response.write(blobId); - } + } } } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/server/Request.java b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/Request.java similarity index 90% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/server/Request.java rename to oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/Request.java index f9a5fe6f756..679551cb859 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/server/Request.java +++ b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/Request.java @@ -25,54 +25,54 @@ import java.util.LinkedHashMap; import java.util.Map; -import org.apache.jackrabbit.mk.util.BoundedInputStream; -import org.apache.jackrabbit.mk.util.ChunkedInputStream; +import org.apache.jackrabbit.mk.remote.util.BoundedInputStream; +import org.apache.jackrabbit.mk.remote.util.ChunkedInputStream; import org.apache.jackrabbit.mk.util.IOUtils; /** * HTTP Request implementation. */ class Request implements Closeable { - + private static final String HTTP_11_PROTOCOL = "HTTP/1.1"; private InputStream in; - + private String method; - + private String file; - + private String queryString; - + private String protocol; - - private final Map headers = new LinkedHashMap(); - + + private final Map headers = new LinkedHashMap(); + private boolean paramsChecked; - - private final Map params = new LinkedHashMap(); - + + private final Map params = new LinkedHashMap(); + private final ChunkedInputStream chunkedIn = new ChunkedInputStream(null); private InputStream reqIn; - + /** * Parse a request. This automatically resets any internal state, so it can be * used multiple times - * + * * @param in input stream * @throws IOException if an I/O error occurs */ void parse(InputStream in) throws IOException { String requestLine = readLine(in); - + String[] parts = requestLine.split(" "); if (parts.length != 3) { String msg = String.format("Bad HTTP request line: %s", requestLine); throw new IOException(msg); } method = parts[0]; - + String uri = parts[1]; int index = uri.lastIndexOf('?'); if (index == -1) { @@ -82,11 +82,11 @@ void parse(InputStream in) throws IOException { file = uri.substring(0, index); queryString = uri.substring(index + 1); } - + protocol = parts[2]; - + headers.clear(); - + for (;;) { String headerLine = readLine(in); if (headerLine.length() == 0) { @@ -94,26 +94,26 @@ void parse(InputStream in) throws IOException { } parts = headerLine.split(":"); if (parts.length == 2) { - headers.put(parts[0].trim(), parts[1].trim()); + headers.put(parts[0].trim().toLowerCase(), parts[1].trim()); } } - + params.clear(); paramsChecked = false; reqIn = null; - + this.in = in; } - + /** - * Read a single line, terminated by a CR LF combination from an InputStream. - * + * Read a single line, terminated by a CR LF combination from an {@code InputStream}. + * * @return line * @throws IOException if an I/O error occurs */ private static String readLine(InputStream in) throws IOException { StringBuilder line = new StringBuilder(128); - + for (;;) { int c = in.read(); switch (c) { @@ -129,7 +129,7 @@ private static String readLine(InputStream in) throws IOException { } } } - + public String getMethod() { return method; } @@ -137,9 +137,9 @@ public String getMethod() { public String getFile() { return file; } - + private String getContentType() { - String ct = headers.get("Content-Type"); + String ct = headers.get("content-type"); if (ct != null) { int sep = ct.indexOf(';'); if (sep != -1) { @@ -148,9 +148,9 @@ private String getContentType() { } return ct; } - + private int getContentLength() { - String s = headers.get("Content-Length"); + String s = headers.get("content-length"); if (s != null) { try { return Integer.parseInt(s); @@ -161,10 +161,11 @@ private int getContentLength() { return -1; } - public Map getHeaders() { - return headers; + public String getUserAgent() { + return headers.get("user-agent"); } - + + public String getQueryString() { return queryString; } @@ -184,7 +185,7 @@ public String getParameter(String name) throws IOException { } return params.get(name); } - + public String getParameter(String name, String defaultValue) throws IOException { String s = getParameter(name); if (s != null) { @@ -240,7 +241,7 @@ public InputStream getFileParameter(String name) throws IOException { return new BoundaryInputStream(body, boundary); } - private static void collectParameters(String s, Map map) throws IOException { + private static void collectParameters(String s, Map map) throws IOException { for (String param : s.split("&")) { String[] nv = param.split("=", 2); if (nv.length == 2) { @@ -248,10 +249,10 @@ private static void collectParameters(String s, Map map) throws I } } } - + public InputStream getInputStream() { if (reqIn == null) { - String encoding = headers.get("Transfer-Encoding"); + String encoding = headers.get("transfer-encoding"); if ("chunked".equalsIgnoreCase(encoding)) { chunkedIn.recycle(in); reqIn = chunkedIn; @@ -265,11 +266,12 @@ public InputStream getInputStream() { } return reqIn; } - + boolean isKeepAlive() { return HTTP_11_PROTOCOL.equals(protocol); } - + + @Override public void close() { if (in != null) { try { diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/server/Response.java b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/Response.java similarity index 93% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/server/Response.java rename to oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/Response.java index 10f0d00f8af..da5a4b88989 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/server/Response.java +++ b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/Response.java @@ -24,36 +24,36 @@ import org.apache.jackrabbit.mk.util.IOUtils; -import static org.apache.jackrabbit.mk.util.ChunkedInputStream.MAX_CHUNK_SIZE; +import static org.apache.jackrabbit.mk.remote.util.ChunkedInputStream.MAX_CHUNK_SIZE; /** * HTTP Response implementation. */ class Response implements Closeable { - + private OutputStream out; - + private boolean keepAlive; - + private boolean headersSent; - + private boolean committed; - + private boolean chunked; - + private int statusCode; - + private String contentType; - + private final BodyOutputStream bodyOut = new BodyOutputStream(); - + private OutputStream respOut; - - private final Map headers = new LinkedHashMap(); - + + private final Map headers = new LinkedHashMap(); + /** * Recycle this instance, using another output stream and a keep-alive flag. - * + * * @param out output stream * @param keepAlive whether to keep alive the connection */ @@ -68,10 +68,10 @@ void recycle(OutputStream out, boolean keepAlive) { respOut = null; headers.clear(); } - + /** * Return the status message associated with a status code. - * + * * @param sc status code * @return associated status message */ @@ -89,14 +89,14 @@ private static String getStatusMsg(int sc) { return "Internal server error"; } } - + private void sendHeaders() throws IOException { if (headersSent) { return; } - + headersSent = true; - + int statusCode = this.statusCode; if (statusCode == 0) { statusCode = 200; @@ -114,9 +114,9 @@ private void sendHeaders() throws IOException { setContentType("text/html"); write(body); } - + writeLine(String.format("HTTP/1.1 %d %s", statusCode, msg)); - + if (committed) { writeLine(String.format("Content-Length: %d", bodyOut.getCount())); } else { @@ -134,21 +134,22 @@ private void sendHeaders() throws IOException { writeLine(String.format("%s: %s", header.getKey(), header.getValue())); } } - + writeLine(""); - + if (out != null) { out.flush(); } } + @Override public void close() throws IOException { committed = true; - + try { sendHeaders(); IOUtils.closeQuietly(respOut); - + if (out != null) { out.flush(); } @@ -156,7 +157,7 @@ public void close() throws IOException { out = null; } } - + private void writeLine(String s) throws IOException { if (out == null) { return; @@ -187,7 +188,7 @@ void writeBody(byte[] b, int off, int len) throws IOException { out.write(("\r\n").getBytes()); } } - + public void setContentType(String contentType) { this.contentType = contentType; } @@ -202,32 +203,32 @@ public OutputStream getOutputStream() { public void setStatusCode(int statusCode) { this.statusCode = statusCode; } - + public void addHeader(String name, String value) { headers.put(name, value); } - + public void write(String s) throws IOException { getOutputStream().write(s.getBytes("8859_1")); } - + /** - * Internal OutputStream passed to servlet handlers. + * Internal {@code OutputStream} passed to servlet handlers. */ class BodyOutputStream extends OutputStream { - + /** * Buffer size chosen intentionally to not exceed maximum chunk * size we'd like to transmit. */ private final byte[] buf = new byte[MAX_CHUNK_SIZE]; - + private int offset; /** * Return the number of valid bytes in the buffer. - * - * @return number of bytes + * + * @return number of bytes */ public int getCount() { return offset; @@ -256,7 +257,7 @@ public void write(byte[] b, int off, int len) throws IOException { offset += n; } } - + @Override public void flush() throws IOException { if (offset > 0) { @@ -264,15 +265,15 @@ public void flush() throws IOException { offset = 0; } } - + public void reset() { offset = 0; } - + @Override public void close() throws IOException { flush(); - + writeBody(buf, 0, 0); } } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/server/Server.java b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/Server.java similarity index 82% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/server/Server.java rename to oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/Server.java index 1df8fd86229..672ac930d02 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/server/Server.java +++ b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/Server.java @@ -22,7 +22,9 @@ import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; +import java.net.SocketAddress; import java.net.SocketTimeoutException; +import java.net.UnknownHostException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; @@ -30,11 +32,10 @@ import javax.net.ServerSocketFactory; -import org.apache.jackrabbit.mk.MicroKernelFactory; import org.apache.jackrabbit.mk.api.MicroKernel; /** - * Server exposing a MicroKernel. + * Server exposing a {@code MicroKernel}. */ public class Server { @@ -113,6 +114,7 @@ public void start() throws IOException { es = createExecutorService(); new Thread(new Runnable() { + @Override public void run() { accept(); } @@ -124,6 +126,7 @@ void accept() { while (!stopped.get()) { final Socket socket = ss.accept(); es.execute(new Runnable() { + @Override public void run() { process(socket); } @@ -139,7 +142,7 @@ private ServerSocket createServerSocket() throws IOException { } private ExecutorService createExecutorService() { - return Executors.newFixedThreadPool(10); + return Executors.newCachedThreadPool(); } /** @@ -155,6 +158,7 @@ void process(Socket socket) { } HttpProcessor processor = new HttpProcessor(socket, new Servlet() { + @Override public void service(Request request, Response response) throws IOException { Server.this.service(request, response); @@ -190,13 +194,28 @@ void service(Request request, Response response) throws IOException { /** * Return the server's local socket address. * - * @return socket address or null if the server is not started + * @return socket address or {@code null} if the server is not started */ public InetSocketAddress getAddress() { if (!started.get() || stopped.get()) { return null; } - return (InetSocketAddress) ss.getLocalSocketAddress(); + SocketAddress address = ss.getLocalSocketAddress(); + if (address instanceof InetSocketAddress) { + InetSocketAddress isa = (InetSocketAddress) address; + if (isa.getAddress().isAnyLocalAddress()) { + try { + return new InetSocketAddress( + InetAddress.getByName("localhost"), + ss.getLocalPort()); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } else { + return isa; + } + } + return null; } /** @@ -206,10 +225,6 @@ public void stop() { if (!stopped.compareAndSet(false, true)) { return; } - MicroKernel mk = mkref.getAndSet(null); - if (mk != null) { - mk.dispose(); - } if (es != null) { es.shutdown(); } @@ -222,30 +237,4 @@ public void stop() { } } - public static void main(String[] args) throws Exception { - if (args.length == 0) { - System.out.println(String.format("usage: %s microkernel-url [port] [bindaddr]", - Server.class.getName())); - return; - } - - MicroKernel mk = MicroKernelFactory.getInstance(args[0]); - - final Server server = new Server(mk); - if (args.length >= 2) { - server.setPort(Integer.parseInt(args[1])); - } else { - server.setPort(28080); - } - if (args.length >= 3) { - server.setBindAddress(InetAddress.getByName(args[2])); - } - server.start(); - - Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { - public void run() { - server.stop(); - } - }, "ShutdownHook")); - } } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/server/Servlet.java b/oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/Servlet.java similarity index 100% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/server/Servlet.java rename to oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/Servlet.java diff --git a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/bg-body.png b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/bg-body.png similarity index 100% rename from oak-core/src/main/resources/org/apache/jackrabbit/mk/server/bg-body.png rename to oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/bg-body.png diff --git a/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/branch.html b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/branch.html new file mode 100644 index 00000000000..8c19a1b98c9 --- /dev/null +++ b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/branch.html @@ -0,0 +1,43 @@ + + + +µKernel prototype: branch + + + + + + +

    +

    Write Operations: branch

    +
    + + + + + +
    Trunk Revision ID
      +
    +
    +

    + +

    + +
    + + diff --git a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/commit.html b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/commit.html similarity index 63% rename from oak-core/src/main/resources/org/apache/jackrabbit/mk/server/commit.html rename to oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/commit.html index 10f0ed80211..1c521e585ff 100644 --- a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/commit.html +++ b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/commit.html @@ -1,4 +1,20 @@ + µKernel prototype: commit diff --git a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/diff.html b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/diff.html similarity index 54% rename from oak-core/src/main/resources/org/apache/jackrabbit/mk/server/diff.html rename to oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/diff.html index e21f7b1276b..b3f7ef36cec 100644 --- a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/diff.html +++ b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/diff.html @@ -1,4 +1,20 @@ + µKernel prototype: diff @@ -20,8 +36,8 @@

    Revision Operations: diff

    - Path - + Filter +  
    diff --git a/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/footer.js b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/footer.js new file mode 100644 index 00000000000..58a32233340 --- /dev/null +++ b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/footer.js @@ -0,0 +1,17 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +document.write(''); diff --git a/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/getChildNodeCount.html b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/getChildNodeCount.html new file mode 100644 index 00000000000..e8d118f62e9 --- /dev/null +++ b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/getChildNodeCount.html @@ -0,0 +1,47 @@ + + + +µKernel prototype: getLength + + + + + + +
    +

    Read Operations: getChildNodeCount

    +
    + + + + + + + + + +
    Path
    Revision ID
      +
    +
    +

    + +

    + +
    + + diff --git a/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/getHeadRevision.html b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/getHeadRevision.html new file mode 100644 index 00000000000..ed70534f965 --- /dev/null +++ b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/getHeadRevision.html @@ -0,0 +1,39 @@ + + + +µKernel prototype: getHeadRevision + + + + + + +
    +

    Revision Operations: getHeadRevision

    +
    + +
      +
    +
    +

    + +

    + +
    + + diff --git a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/getJournal.html b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/getJournal.html similarity index 52% rename from oak-core/src/main/resources/org/apache/jackrabbit/mk/server/getJournal.html rename to oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/getJournal.html index 8669d45cee2..44de55bfa66 100644 --- a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/getJournal.html +++ b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/getJournal.html @@ -1,4 +1,20 @@ + µKernel prototype: getJournal @@ -19,6 +35,10 @@

    Revision Operations: getJournal

    To Revision ID + + Filter + +  
    diff --git a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/getLength.html b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/getLength.html similarity index 51% rename from oak-core/src/main/resources/org/apache/jackrabbit/mk/server/getLength.html rename to oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/getLength.html index 43c98ff97ed..cfe2d13de32 100644 --- a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/getLength.html +++ b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/getLength.html @@ -1,4 +1,20 @@ + µKernel prototype: getLength diff --git a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/getNodes.html b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/getNodes.html similarity index 60% rename from oak-core/src/main/resources/org/apache/jackrabbit/mk/server/getNodes.html rename to oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/getNodes.html index dcf85262246..a27bbebf73d 100644 --- a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/getNodes.html +++ b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/getNodes.html @@ -1,4 +1,20 @@ + µKernel prototype: getNodes @@ -31,6 +47,10 @@

    Read Operations: getNodes

    Count + + Filter + +  
    diff --git a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/getRevisions.html b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/getRevisionHistory.html similarity index 50% rename from oak-core/src/main/resources/org/apache/jackrabbit/mk/server/getRevisions.html rename to oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/getRevisionHistory.html index efbf52cb8c6..7e955e22b3c 100644 --- a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/getRevisions.html +++ b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/getRevisionHistory.html @@ -1,6 +1,22 @@ + -µKernel prototype: getRevisions +µKernel prototype: getRevisionHistory @@ -8,7 +24,7 @@
    -

    Revision Operations: getRevisions

    +

    Revision Operations: getRevisionHistory

    diff --git a/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/header.js b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/header.js new file mode 100644 index 00000000000..7658c5f00e6 --- /dev/null +++ b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/header.js @@ -0,0 +1,17 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +document.write('
    µKernel
    prototype
    '); diff --git a/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/index.html b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/index.html new file mode 100644 index 00000000000..c27fce121e6 --- /dev/null +++ b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/index.html @@ -0,0 +1,53 @@ + + + +µKernel prototype + + + + + + +
    +

    +

    Revision Operations

    + getHeadRevision
    + getRevisionHistory
    + waitForCommit
    + getJournal
    + diff
    + +

    Read Operations

    + nodeExists
    + getNodes
    + getChildNodeCount
    + +

    Write Operations

    + commit
    + branch
    + merge
    + +

    Blob Operations

    + getLength
    + read
    + write
    +

    +
    + + + diff --git a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/logo.png b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/logo.png similarity index 100% rename from oak-core/src/main/resources/org/apache/jackrabbit/mk/server/logo.png rename to oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/logo.png diff --git a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/main.css b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/main.css similarity index 72% rename from oak-core/src/main/resources/org/apache/jackrabbit/mk/server/main.css rename to oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/main.css index cd9caeb670d..4fb9ffb8a1e 100644 --- a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/main.css +++ b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/main.css @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + body { background: #eee url(bg-body.png) top center repeat-y; color: #333; diff --git a/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/merge.html b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/merge.html new file mode 100644 index 00000000000..2dd7e6d960e --- /dev/null +++ b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/merge.html @@ -0,0 +1,47 @@ + + + +µKernel prototype: branch + + + + + + +
    +

    Write Operations: merge

    + +
    + + + + + + + + +
    Branch Revision ID
    Message
      +
    +
    +

    + +

    + +
    + + diff --git a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/nodeExists.html b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/nodeExists.html similarity index 55% rename from oak-core/src/main/resources/org/apache/jackrabbit/mk/server/nodeExists.html rename to oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/nodeExists.html index ffd419be7ea..007d5eb079a 100644 --- a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/nodeExists.html +++ b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/nodeExists.html @@ -1,4 +1,20 @@ + µKernel prototype: nodeExists diff --git a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/read.html b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/read.html similarity index 51% rename from oak-core/src/main/resources/org/apache/jackrabbit/mk/server/read.html rename to oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/read.html index 6dcdddfdc66..74305d7ef15 100644 --- a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/read.html +++ b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/read.html @@ -1,4 +1,20 @@ + µKernel prototype: read diff --git a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/waitForCommit.html b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/waitForCommit.html similarity index 56% rename from oak-core/src/main/resources/org/apache/jackrabbit/mk/server/waitForCommit.html rename to oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/waitForCommit.html index babcfef22ba..c3c6b4f06f3 100644 --- a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/waitForCommit.html +++ b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/waitForCommit.html @@ -1,4 +1,20 @@ + µKernel prototype: waitForCommit diff --git a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/write.html b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/write.html similarity index 51% rename from oak-core/src/main/resources/org/apache/jackrabbit/mk/server/write.html rename to oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/write.html index 8c82e6ce298..06c3422dc68 100644 --- a/oak-core/src/main/resources/org/apache/jackrabbit/mk/server/write.html +++ b/oak-mk-remote/src/main/resources/org/apache/jackrabbit/mk/server/write.html @@ -1,4 +1,20 @@ + µKernel prototype: write diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/server/BoundaryInputStreamTest.java b/oak-mk-remote/src/test/java/org/apache/jackrabbit/mk/server/BoundaryInputStreamTest.java similarity index 100% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/server/BoundaryInputStreamTest.java rename to oak-mk-remote/src/test/java/org/apache/jackrabbit/mk/server/BoundaryInputStreamTest.java diff --git a/oak-mk/pom.xml b/oak-mk/pom.xml new file mode 100644 index 00000000000..eca6a2ff99e --- /dev/null +++ b/oak-mk/pom.xml @@ -0,0 +1,143 @@ + + + + + + 4.0.0 + + + org.apache.jackrabbit + oak-parent + 0.6-SNAPSHOT + ../oak-parent/pom.xml + + + oak-mk + Oak MicroKernel + bundle + + + + + org.apache.felix + maven-bundle-plugin + + + + org.apache.jackrabbit.mk.json, + org.apache.jackrabbit.mk.util, + org.apache.jackrabbit.mk.core, + org.apache.jackrabbit.mk.blobs + + + + + + org.apache.felix + maven-scr-plugin + + + + + + + + org.osgi + org.osgi.core + provided + + + org.osgi + org.osgi.compendium + provided + + + biz.aQute + bndlib + provided + + + org.apache.felix + org.apache.felix.scr.annotations + provided + + + + + org.apache.jackrabbit + oak-mk-api + ${project.version} + + + + + org.apache.jackrabbit + oak-commons + ${project.version} + + + + + org.slf4j + slf4j-api + 1.6.4 + + + + + com.h2database + h2 + 1.3.158 + true + + + + + com.google.code.findbugs + jsr305 + 2.0.0 + provided + + + + + com.googlecode.json-simple + json-simple + 1.1 + test + + + junit + junit + test + + + ch.qos.logback + logback-classic + 1.0.1 + test + + + commons-io + commons-io + 1.4 + test + + + + diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/AbstractBlobStore.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/blobs/AbstractBlobStore.java similarity index 68% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/AbstractBlobStore.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/blobs/AbstractBlobStore.java index 6bf8f157f97..a144735c6bd 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/AbstractBlobStore.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/blobs/AbstractBlobStore.java @@ -16,14 +16,14 @@ */ package org.apache.jackrabbit.mk.blobs; -import org.apache.jackrabbit.mk.fs.FilePath; -import org.apache.jackrabbit.mk.util.ExceptionFactory; -import org.apache.jackrabbit.mk.util.IOUtils; import org.apache.jackrabbit.mk.util.Cache; +import org.apache.jackrabbit.mk.util.IOUtils; import org.apache.jackrabbit.mk.util.StringUtils; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; @@ -64,11 +64,13 @@ */ public abstract class AbstractBlobStore implements BlobStore, Cache.Backend { - protected static final String HASH_ALGORITHM = "SHA-1"; + protected static final String HASH_ALGORITHM = "SHA-256"; protected static final int TYPE_DATA = 0; protected static final int TYPE_HASH = 1; protected static final int TYPE_HASH_COMPRESSED = 2; + + protected static final int BLOCK_SIZE_LIMIT = 48; protected Map> inUse = Collections.synchronizedMap(new WeakHashMap>()); @@ -77,7 +79,7 @@ public abstract class AbstractBlobStore implements BlobStore, Cache.Backend cache = Cache.newInstance(this, 8 * 1024 * 1024); public void setBlockSizeMin(int x) { + validateBlockSize(x); this.blockSizeMin = x; } @@ -96,28 +99,45 @@ public long getBlockSizeMin() { } public void setBlockSize(int x) { + validateBlockSize(x); this.blockSize = x; } + + private static void validateBlockSize(int x) { + if (x < BLOCK_SIZE_LIMIT) { + throw new IllegalArgumentException( + "The minimum size must be bigger " + + "than a content hash itself; limit = " + BLOCK_SIZE_LIMIT); + } + } public int getBlockSize() { return blockSize; } - public String addBlob(String tempFilePath) { + /** + * Write a blob from a temporary file. The temporary file is removed + * afterwards. A file based blob stores might simply rename the file, so + * that no additional writes are necessary. + * + * @param tempFilePath the temporary file + * @return the blob id + */ + public String writeBlob(String tempFilePath) throws Exception { + File file = new File(tempFilePath); + InputStream in = null; try { - FilePath file = FilePath.get(tempFilePath); - try { - InputStream in = file.newInputStream(); - return writeBlob(in); - } finally { - file.delete(); + in = new FileInputStream(file); + return writeBlob(in); + } finally { + if (in != null) { + in.close(); } - } catch (Exception e) { - throw ExceptionFactory.convert(e); + file.delete(); } } - public String writeBlob(InputStream in) { + public String writeBlob(InputStream in) throws Exception { try { ByteArrayOutputStream idStream = new ByteArrayOutputStream(); convertBlobToId(in, idStream, 0, 0); @@ -126,13 +146,12 @@ public String writeBlob(InputStream in) { String blobId = StringUtils.convertBytesToHex(id); usesBlobId(blobId); return blobId; - } catch (Exception e) { + } finally { try { in.close(); - } catch (IOException e1) { + } catch (IOException e) { // ignore } - throw ExceptionFactory.convert(e); } } @@ -215,59 +234,55 @@ protected void markInUse() throws Exception { } } - public int readBlob(String blobId, long pos, byte[] buff, int off, int length) { - try { - if (isMarkEnabled()) { - mark(blobId); - } - byte[] id = StringUtils.convertHexToBytes(blobId); - ByteArrayInputStream idStream = new ByteArrayInputStream(id); - while (true) { - int type = idStream.read(); - if (type == -1) { - return -1; - } else if (type == TYPE_DATA) { - int len = IOUtils.readVarInt(idStream); - if (pos < len) { - IOUtils.skipFully(idStream, (int) pos); - len -= pos; - if (length < len) { - len = length; - } - IOUtils.readFully(idStream, buff, off, len); - return len; + public int readBlob(String blobId, long pos, byte[] buff, int off, int length) throws Exception { + if (isMarkEnabled()) { + mark(blobId); + } + byte[] id = StringUtils.convertHexToBytes(blobId); + ByteArrayInputStream idStream = new ByteArrayInputStream(id); + while (true) { + int type = idStream.read(); + if (type == -1) { + return -1; + } else if (type == TYPE_DATA) { + int len = IOUtils.readVarInt(idStream); + if (pos < len) { + IOUtils.skipFully(idStream, (int) pos); + len -= pos; + if (length < len) { + len = length; } - IOUtils.skipFully(idStream, len); - pos -= len; - } else if (type == TYPE_HASH) { - int level = IOUtils.readVarInt(idStream); - long totalLength = IOUtils.readVarLong(idStream); + IOUtils.readFully(idStream, buff, off, len); + return len; + } + IOUtils.skipFully(idStream, len); + pos -= len; + } else if (type == TYPE_HASH) { + int level = IOUtils.readVarInt(idStream); + long totalLength = IOUtils.readVarLong(idStream); + if (level > 0) { + // block length (ignored) + IOUtils.readVarLong(idStream); + } + byte[] digest = new byte[IOUtils.readVarInt(idStream)]; + IOUtils.readFully(idStream, digest, 0, digest.length); + if (pos >= totalLength) { + pos -= totalLength; + } else { if (level > 0) { - // block length (ignored) - IOUtils.readVarLong(idStream); - } - byte[] digest = new byte[IOUtils.readVarInt(idStream)]; - IOUtils.readFully(idStream, digest, 0, digest.length); - if (pos >= totalLength) { - pos -= totalLength; + byte[] block = readBlock(digest, 0); + idStream = new ByteArrayInputStream(block); } else { - if (level > 0) { - byte[] block = readBlock(digest, 0); - idStream = new ByteArrayInputStream(block); - } else { - long readPos = pos - pos % blockSize; - byte[] block = readBlock(digest, readPos); - ByteArrayInputStream in = new ByteArrayInputStream(block); - IOUtils.skipFully(in, pos - readPos); - return IOUtils.readFully(in, buff, off, length); - } + long readPos = pos - pos % blockSize; + byte[] block = readBlock(digest, readPos); + ByteArrayInputStream in = new ByteArrayInputStream(block); + IOUtils.skipFully(in, pos - readPos); + return IOUtils.readFully(in, buff, off, length); } - } else { - throw new IOException("Unknown blobs id type " + type + " for blob " + blobId); } + } else { + throw new IOException("Unknown blobs id type " + type + " for blob " + blobId); } - } catch (Exception e) { - throw ExceptionFactory.convert(e); } } @@ -277,49 +292,57 @@ private byte[] readBlock(byte[] digest, long pos) throws Exception { } public Data load(BlockId id) { + byte[] data; try { - return new Data(readBlockFromBackend(id)); + data = readBlockFromBackend(id); } catch (Exception e) { - throw ExceptionFactory.convert(e); + throw new RuntimeException("failed to read block from backend, id " + id, e); } + if (data == null) { + throw new IllegalArgumentException("The block with id " + id + " was not found"); + } + return new Data(data); } + /** + * Load the block from the storage backend. Returns null if the block was + * not found. + * + * @param id the block id + * @return the block data, or null + */ protected abstract byte[] readBlockFromBackend(BlockId id) throws Exception; - public long getBlobLength(String blobId) { - try { - if (isMarkEnabled()) { - mark(blobId); + public long getBlobLength(String blobId) throws IOException { + if (isMarkEnabled()) { + mark(blobId); + } + byte[] id = StringUtils.convertHexToBytes(blobId); + ByteArrayInputStream idStream = new ByteArrayInputStream(id); + long totalLength = 0; + while (true) { + int type = idStream.read(); + if (type == -1) { + break; } - byte[] id = StringUtils.convertHexToBytes(blobId); - ByteArrayInputStream idStream = new ByteArrayInputStream(id); - long totalLength = 0; - while (true) { - int type = idStream.read(); - if (type == -1) { - break; - } - if (type == TYPE_DATA) { - int len = IOUtils.readVarInt(idStream); - IOUtils.skipFully(idStream, len); - totalLength += len; - } else if (type == TYPE_HASH) { - int level = IOUtils.readVarInt(idStream); - totalLength += IOUtils.readVarLong(idStream); - if (level > 0) { - // block length (ignored) - IOUtils.readVarLong(idStream); - } - int digestLength = IOUtils.readVarInt(idStream); - IOUtils.skipFully(idStream, digestLength); - } else { - throw new IOException("Datastore id type " + type + " for blob " + blobId); + if (type == TYPE_DATA) { + int len = IOUtils.readVarInt(idStream); + IOUtils.skipFully(idStream, len); + totalLength += len; + } else if (type == TYPE_HASH) { + int level = IOUtils.readVarInt(idStream); + totalLength += IOUtils.readVarLong(idStream); + if (level > 0) { + // block length (ignored) + IOUtils.readVarLong(idStream); } + int digestLength = IOUtils.readVarInt(idStream); + IOUtils.skipFully(idStream, digestLength); + } else { + throw new IOException("Datastore id type " + type + " for blob " + blobId); } - return totalLength; - } catch (IOException e) { - throw ExceptionFactory.convert(e); } + return totalLength; } protected void mark(String blobId) throws IOException { @@ -339,7 +362,7 @@ private void mark(ByteArrayInputStream idStream) throws Exception { return; } else if (type == TYPE_DATA) { int len = IOUtils.readVarInt(idStream); - IOUtils.skipFully(idStream, (int) len); + IOUtils.skipFully(idStream, len); } else if (type == TYPE_HASH) { int level = IOUtils.readVarInt(idStream); // totalLength @@ -364,10 +387,6 @@ private void mark(ByteArrayInputStream idStream) throws Exception { } } - public void close() { - // ignore - } - /** * A block id. Blocks are small enough to fit in memory, so they can be * cached. @@ -386,6 +405,9 @@ public boolean equals(Object other) { if (this == other) { return true; } + if (other == null || !(other instanceof BlockId)) { + return false; + } BlockId o = (BlockId) other; return Arrays.equals(digest, o.digest) && pos == o.pos; diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/BlobStore.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/blobs/BlobStore.java similarity index 75% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/BlobStore.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/blobs/BlobStore.java index e56e6cb728f..61acec0d8db 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/BlobStore.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/blobs/BlobStore.java @@ -23,16 +23,6 @@ */ public interface BlobStore { - /** - * Write a blob from a temporary file. The temporary file is removed - * afterwards. A file based blob stores might simply rename the file, so - * that no additional writes are necessary. - * - * @param tempFilePath the temporary file - * @return the blob id - */ - String addBlob(String tempFilePath) throws Exception; - /** * Write a blob from an input stream. * This method closes the input stream. @@ -42,10 +32,24 @@ public interface BlobStore { */ String writeBlob(InputStream in) throws Exception; + /** + * Read a number of bytes from a blob. + * + * @param blobId the blob id + * @param pos the position within the blob + * @param buff the target byte array + * @param off the offset within the target array + * @param length the number of bytes to read + * @return the number of bytes read + */ int readBlob(String blobId, long pos, byte[] buff, int off, int length) throws Exception; + /** + * Get the length of the blob. + * + * @param blobId the blob id + * @return the length + */ long getBlobLength(String blobId) throws Exception; - void close(); - } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/BlobStoreInputStream.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/blobs/BlobStoreInputStream.java similarity index 100% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/BlobStoreInputStream.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/blobs/BlobStoreInputStream.java diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/DbBlobStore.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/blobs/DbBlobStore.java similarity index 98% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/DbBlobStore.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/blobs/DbBlobStore.java index f7d64c8c6d2..b37bb619c48 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/DbBlobStore.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/blobs/DbBlobStore.java @@ -43,6 +43,7 @@ public void setConnectionPool(JdbcConnectionPool cp) throws SQLException { "(id varchar primary key, level int, lastMod bigint)"); stat.execute("create table if not exists datastore_data" + "(id varchar primary key, data binary)"); + stat.close(); conn.close(); } @@ -154,6 +155,7 @@ protected void mark(BlockId blockId) throws Exception { prep.setString(2, id); prep.setLong(3, minLastModified); prep.executeUpdate(); + prep.close(); } finally { conn.close(); } @@ -183,6 +185,8 @@ public int sweep() throws Exception { prepData.execute(); count++; } + prepData.close(); + prep.close(); } finally { conn.close(); } diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/blobs/FileBlobStore.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/blobs/FileBlobStore.java new file mode 100644 index 00000000000..4ac6a640406 --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/blobs/FileBlobStore.java @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.blobs; + +import org.apache.jackrabbit.mk.util.IOUtils; +import org.apache.jackrabbit.mk.util.StringUtils; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.DigestInputStream; +import java.security.MessageDigest; + +/** + * A file blob store. + */ +public class FileBlobStore extends AbstractBlobStore { + + private static final String OLD_SUFFIX = "_old"; + + private final File baseDir; + private final byte[] buffer = new byte[16 * 1024]; + private boolean mark; + + // TODO file operations are not secure (return values not checked, no retry,...) + + public FileBlobStore(String dir) throws IOException { + baseDir = new File(dir); + baseDir.mkdirs(); + } + + @Override + public String writeBlob(String tempFilePath) throws Exception { + File file = new File(tempFilePath); + InputStream in = new FileInputStream(file); + MessageDigest messageDigest = MessageDigest.getInstance(HASH_ALGORITHM); + DigestInputStream din = new DigestInputStream(in, messageDigest); + long length = file.length(); + try { + while (true) { + int len = din.read(buffer, 0, buffer.length); + if (len < 0) { + break; + } + } + } finally { + din.close(); + } + ByteArrayOutputStream idStream = new ByteArrayOutputStream(); + idStream.write(TYPE_HASH); + IOUtils.writeVarInt(idStream, 0); + IOUtils.writeVarLong(idStream, length); + byte[] digest = messageDigest.digest(); + File f = getFile(digest, false); + if (f.exists()) { + file.delete(); + } else { + File parent = f.getParentFile(); + if (!parent.exists()) { + parent.mkdirs(); + } + file.renameTo(f); + } + IOUtils.writeVarInt(idStream, digest.length); + idStream.write(digest); + byte[] id = idStream.toByteArray(); + String blobId = StringUtils.convertBytesToHex(id); + usesBlobId(blobId); + return blobId; + } + + @Override + protected synchronized void storeBlock(byte[] digest, int level, byte[] data) throws IOException { + File f = getFile(digest, false); + if (f.exists()) { + return; + } + File parent = f.getParentFile(); + if (!parent.exists()) { + parent.mkdirs(); + } + File temp = new File(parent, f.getName() + ".temp"); + OutputStream out = new FileOutputStream(temp, false); + out.write(data); + out.close(); + temp.renameTo(f); + } + + private File getFile(byte[] digest, boolean old) { + String id = StringUtils.convertBytesToHex(digest); + String sub1 = id.substring(id.length() - 2); + String sub2 = id.substring(id.length() - 4, id.length() - 2); + if (old) { + sub2 += OLD_SUFFIX; + } + return new File(new File(new File(baseDir, sub1), sub2), id + ".dat"); + } + + @Override + protected byte[] readBlockFromBackend(BlockId id) throws IOException { + File f = getFile(id.digest, false); + if (!f.exists()) { + File old = getFile(id.digest, true); + f.getParentFile().mkdir(); + old.renameTo(f); + f = getFile(id.digest, false); + } + int length = (int) Math.min(f.length(), getBlockSize()); + byte[] data = new byte[length]; + InputStream in = new FileInputStream(f); + try { + IOUtils.skipFully(in, id.pos); + IOUtils.readFully(in, data, 0, length); + } finally { + in.close(); + } + return data; + } + + @Override + public void startMark() throws Exception { + mark = true; + for (int j = 0; j < 256; j++) { + String sub1 = StringUtils.convertBytesToHex(new byte[] { (byte) j }); + File x = new File(baseDir, sub1); + for (int i = 0; i < 256; i++) { + String sub2 = StringUtils.convertBytesToHex(new byte[] { (byte) i }); + File d = new File(x, sub2); + File old = new File(x, sub2 + OLD_SUFFIX); + if (d.exists()) { + if (old.exists()) { + for (File p : d.listFiles()) { + String name = p.getName(); + File newName = new File(old, name); + p.renameTo(newName); + } + } else { + d.renameTo(old); + } + } + } + } + markInUse(); + } + + @Override + protected boolean isMarkEnabled() { + return mark; + } + + @Override + protected void mark(BlockId id) throws IOException { + File f = getFile(id.digest, false); + if (!f.exists()) { + File old = getFile(id.digest, true); + f.getParentFile().mkdir(); + old.renameTo(f); + f = getFile(id.digest, false); + } + } + + @Override + public int sweep() throws IOException { + int count = 0; + for (int j = 0; j < 256; j++) { + String sub1 = StringUtils.convertBytesToHex(new byte[] { (byte) j }); + File x = new File(baseDir, sub1); + for (int i = 0; i < 256; i++) { + String sub = StringUtils.convertBytesToHex(new byte[] { (byte) i }); + File old = new File(x, sub + OLD_SUFFIX); + if (old.exists()) { + for (File p : old.listFiles()) { + String name = p.getName(); + File file = new File(old, name); + file.delete(); + count++; + } + old.delete(); + } + } + } + mark = false; + return count; + } + +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/MemoryBlobStore.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/blobs/MemoryBlobStore.java similarity index 93% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/MemoryBlobStore.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/blobs/MemoryBlobStore.java index e6775cadfb6..4a3ca8710a0 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/blobs/MemoryBlobStore.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/blobs/MemoryBlobStore.java @@ -29,7 +29,11 @@ public class MemoryBlobStore extends AbstractBlobStore { @Override protected byte[] readBlockFromBackend(BlockId id) { - return map.get(id); + byte[] result = map.get(id); + if (result == null) { + result = old.get(id); + } + return result; } @Override diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/core/MicroKernelImpl.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/core/MicroKernelImpl.java new file mode 100644 index 00000000000..6af10eb6ca7 --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/core/MicroKernelImpl.java @@ -0,0 +1,640 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.core; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.api.MicroKernelException; +import org.apache.jackrabbit.mk.json.JsopBuilder; +import org.apache.jackrabbit.mk.json.JsopReader; +import org.apache.jackrabbit.mk.json.JsopTokenizer; +import org.apache.jackrabbit.mk.model.Commit; +import org.apache.jackrabbit.mk.model.CommitBuilder; +import org.apache.jackrabbit.mk.model.Id; +import org.apache.jackrabbit.mk.json.JsonObject; +import org.apache.jackrabbit.mk.model.StoredCommit; +import org.apache.jackrabbit.mk.model.tree.ChildNode; +import org.apache.jackrabbit.mk.model.tree.DiffBuilder; +import org.apache.jackrabbit.mk.model.tree.NodeState; +import org.apache.jackrabbit.mk.model.tree.PropertyState; +import org.apache.jackrabbit.mk.util.CommitGate; +import org.apache.jackrabbit.mk.util.NameFilter; +import org.apache.jackrabbit.mk.util.NodeFilter; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * + */ +public class MicroKernelImpl implements MicroKernel { + + private static final Logger LOG = LoggerFactory.getLogger(MicroKernelImpl.class); + + protected Repository rep; + private final CommitGate gate = new CommitGate(); + + public MicroKernelImpl(String homeDir) throws MicroKernelException { + init(homeDir); + } + + /** + * Creates a new in-memory kernel instance that doesn't need to be + * explicitly closed, i.e. standard Java garbage collection will take + * care of releasing any acquired resources when no longer needed. + * Useful especially for test cases and other similar scenarios. + */ + public MicroKernelImpl() { + this(new Repository()); + } + + /** + * Alternate constructor, used for testing. + * + * @param rep repository, already initialized + */ + public MicroKernelImpl(Repository rep) { + this.rep = rep; + try { + // initialize commit gate with current head + gate.commit(rep.getHeadRevision().toString()); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + protected void init(String homeDir) throws MicroKernelException { + try { + rep = new Repository(homeDir); + rep.init(); + // initialize commit gate with current head + gate.commit(rep.getHeadRevision().toString()); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + public void dispose() { + gate.commit("end"); + if (rep != null) { + try { + rep.shutDown(); + } catch (Exception ignore) { + // fail silently + } + rep = null; + } + } + + public String getHeadRevision() throws MicroKernelException { + if (rep == null) { + throw new IllegalStateException("this instance has already been disposed"); + } + return getHeadRevisionId().toString(); + } + + /** + * Same as {@code getHeadRevisionId}, with typed {@code Id} return value instead of string. + * + * @see #getHeadRevision() + */ + private Id getHeadRevisionId() throws MicroKernelException { + try { + return rep.getHeadRevision(); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + public String getRevisionHistory(long since, int maxEntries, String path) throws MicroKernelException { + if (rep == null) { + throw new IllegalStateException("this instance has already been disposed"); + } + + path = (path == null || "".equals(path)) ? "/" : path; + boolean filtered = !"/".equals(path); + + maxEntries = maxEntries < 0 ? Integer.MAX_VALUE : maxEntries; + List history = new ArrayList(); + try { + StoredCommit commit = rep.getHeadCommit(); + while (commit != null + && history.size() < maxEntries + && commit.getCommitTS() >= since) { + if (filtered) { + try { + String diff = new DiffBuilder( + rep.getNodeState(commit.getParentId(), "/"), + rep.getNodeState(commit.getId(), "/"), + "/", -1, rep.getRevisionStore(), path).build(); + if (!diff.isEmpty()) { + history.add(commit); + } + } catch (Exception e) { + throw new MicroKernelException(e); + } + } else { + history.add(commit); + } + + Id commitId = commit.getParentId(); + if (commitId == null) { + break; + } + commit = rep.getCommit(commitId); + } + } catch (Exception e) { + throw new MicroKernelException(e); + } + + JsopBuilder buff = new JsopBuilder().array(); + for (int i = history.size() - 1; i >= 0; i--) { + StoredCommit commit = history.get(i); + buff.object(). + key("id").value(commit.getId().toString()). + key("ts").value(commit.getCommitTS()). + key("msg").value(commit.getMsg()). + endObject(); + } + return buff.endArray().toString(); + } + + public String waitForCommit(String oldHeadRevisionId, long maxWaitMillis) throws MicroKernelException, InterruptedException { + return gate.waitForCommit(oldHeadRevisionId, maxWaitMillis); + } + + public String getJournal(String fromRevision, String toRevision, String path) throws MicroKernelException { + if (rep == null) { + throw new IllegalStateException("this instance has already been disposed"); + } + + path = (path == null || "".equals(path)) ? "/" : path; + boolean filtered = !"/".equals(path); + + Id fromRevisionId = Id.fromString(fromRevision); + Id toRevisionId = toRevision == null ? getHeadRevisionId() : Id.fromString(toRevision); + + List commits = new ArrayList(); + try { + StoredCommit toCommit = rep.getCommit(toRevisionId); + if (toCommit.getBranchRootId() != null) { + throw new MicroKernelException("branch revisions are not supported: " + toRevisionId); + } + + Commit fromCommit; + if (toRevisionId.equals(fromRevisionId)) { + fromCommit = toCommit; + } else { + fromCommit = rep.getCommit(fromRevisionId); + if (fromCommit.getCommitTS() > toCommit.getCommitTS()) { + // negative range, return empty journal + return "[]"; + } + } + if (fromCommit.getBranchRootId() != null) { + throw new MicroKernelException("branch revisions are not supported: " + fromRevisionId); + } + + // collect commits, starting with toRevisionId + // and traversing parent commit links until we've reached + // fromRevisionId + StoredCommit commit = toCommit; + while (commit != null) { + commits.add(commit); + if (commit.getId().equals(fromRevisionId)) { + break; + } + Id commitId = commit.getParentId(); + if (commitId == null) { + // inconsistent revision history, ignore silently... + break; + } + commit = rep.getCommit(commitId); + if (commit.getCommitTS() < fromCommit.getCommitTS()) { + // inconsistent revision history, ignore silently... + break; + } + } + } catch (MicroKernelException e) { + // re-throw + throw e; + } catch (Exception e) { + throw new MicroKernelException(e); + } + + JsopBuilder commitBuff = new JsopBuilder().array(); + // iterate over commits in chronological order, + // starting with oldest commit + for (int i = commits.size() - 1; i >= 0; i--) { + StoredCommit commit = commits.get(i); + if (commit.getParentId() == null) { + continue; + } + String diff = commit.getChanges(); + if (filtered) { + try { + diff = new DiffBuilder( + rep.getNodeState(commit.getParentId(), "/"), + rep.getNodeState(commit.getId(), "/"), + "/", -1, rep.getRevisionStore(), path).build(); + if (diff.isEmpty()) { + continue; + } + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + commitBuff.object(). + key("id").value(commit.getId().toString()). + key("ts").value(commit.getCommitTS()). + key("msg").value(commit.getMsg()). + key("changes").value(diff).endObject(); + } + return commitBuff.endArray().toString(); + } + + public String diff(String fromRevision, String toRevision, String path, int depth) throws MicroKernelException { + path = (path == null || "".equals(path)) ? "/" : path; + + if (depth < -1) { + throw new IllegalArgumentException("depth"); + } + + Id fromRevisionId, toRevisionId; + if (fromRevision == null || toRevision == null) { + Id head = getHeadRevisionId(); + fromRevisionId = fromRevision == null ? head : Id.fromString(fromRevision); + toRevisionId = toRevision == null ? head : Id.fromString(toRevision); + } else { + fromRevisionId = Id.fromString(fromRevision); + toRevisionId = Id.fromString(toRevision); + } + + if (fromRevisionId.equals(toRevisionId)) { + return ""; + } + + try { + if ("/".equals(path)) { + StoredCommit toCommit = rep.getCommit(toRevisionId); + if (toCommit.getParentId().equals(fromRevisionId)) { + // specified range spans a single commit: + // use diff stored in commit instead of building it dynamically + return toCommit.getChanges(); + } + } + NodeState before = rep.getNodeState(fromRevisionId, path); + NodeState after = rep.getNodeState(toRevisionId, path); + + return new DiffBuilder(before, after, path, depth, rep.getRevisionStore(), path).build(); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + public boolean nodeExists(String path, String revisionId) throws MicroKernelException { + if (rep == null) { + throw new IllegalStateException("this instance has already been disposed"); + } + + Id revId = revisionId == null ? getHeadRevisionId() : Id.fromString(revisionId); + try { + return rep.nodeExists(revId, path); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + public long getChildNodeCount(String path, String revisionId) throws MicroKernelException { + if (rep == null) { + throw new IllegalStateException("this instance has already been disposed"); + } + + Id revId = revisionId == null ? getHeadRevisionId() : Id.fromString(revisionId); + + NodeState nodeState; + try { + nodeState = rep.getNodeState(revId, path); + } catch (Exception e) { + throw new MicroKernelException(e); + } + if (nodeState != null) { + return nodeState.getChildNodeCount(); + } else { + throw new MicroKernelException("Path " + path + " not found in revision " + revisionId); + } + } + + public String getNodes(String path, String revisionId, int depth, long offset, int maxChildNodes, String filter) throws MicroKernelException { + if (rep == null) { + throw new IllegalStateException("this instance has already been disposed"); + } + + Id revId = revisionId == null ? getHeadRevisionId() : Id.fromString(revisionId); + + NodeFilter nodeFilter = filter == null || filter.isEmpty() ? null : NodeFilter.parse(filter); + if (offset > 0 && nodeFilter != null && nodeFilter.getChildNodeFilter() != null) { + // both an offset > 0 and a filter on node names have been specified... + throw new IllegalArgumentException("offset > 0 with child node filter"); + } + + try { + NodeState nodeState = rep.getNodeState(revId, path); + if (nodeState == null) { + return null; + } + + JsopBuilder buf = new JsopBuilder().object(); + toJson(buf, nodeState, depth, (int) offset, maxChildNodes, true, nodeFilter); + return buf.endObject().toString(); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + public String commit(String path, String jsonDiff, String revisionId, String message) throws MicroKernelException { + if (rep == null) { + throw new IllegalStateException("this instance has already been disposed"); + } + if (path.length() > 0 && !PathUtils.isAbsolute(path)) { + throw new IllegalArgumentException("absolute path expected: " + path); + } + + Id revId = revisionId == null ? getHeadRevisionId() : Id.fromString(revisionId); + + try { + JsopTokenizer t = new JsopTokenizer(jsonDiff); + CommitBuilder cb = rep.getCommitBuilder(revId, message); + while (true) { + int r = t.read(); + if (r == JsopReader.END) { + break; + } + int pos; // used for error reporting + switch (r) { + case '+': { + pos = t.getLastPos(); + String subPath = t.readString(); + t.read(':'); + t.read('{'); + String nodePath = PathUtils.concat(path, subPath); + if (!PathUtils.isAbsolute(nodePath)) { + throw new Exception("absolute path expected: " + nodePath + ", pos: " + pos); + } + String parentPath = PathUtils.getParentPath(nodePath); + String nodeName = PathUtils.getName(nodePath); + cb.addNode(parentPath, nodeName, JsonObject.create(t)); + break; + } + case '-': { + pos = t.getLastPos(); + String subPath = t.readString(); + String targetPath = PathUtils.concat(path, subPath); + if (!PathUtils.isAbsolute(targetPath)) { + throw new Exception("absolute path expected: " + targetPath + ", pos: " + pos); + } + cb.removeNode(targetPath); + break; + } + case '^': { + pos = t.getLastPos(); + String subPath = t.readString(); + t.read(':'); + String value; + if (t.matches(JsopReader.NULL)) { + value = null; + } else { + value = t.readRawValue().trim(); + } + String targetPath = PathUtils.concat(path, subPath); + if (!PathUtils.isAbsolute(targetPath)) { + throw new Exception("absolute path expected: " + targetPath + ", pos: " + pos); + } + String parentPath = PathUtils.getParentPath(targetPath); + String propName = PathUtils.getName(targetPath); + cb.setProperty(parentPath, propName, value); + break; + } + case '>': { + pos = t.getLastPos(); + String subPath = t.readString(); + String srcPath = PathUtils.concat(path, subPath); + if (!PathUtils.isAbsolute(srcPath)) { + throw new Exception("absolute path expected: " + srcPath + ", pos: " + pos); + } + t.read(':'); + pos = t.getLastPos(); + String targetPath = t.readString(); + if (!PathUtils.isAbsolute(targetPath)) { + targetPath = PathUtils.concat(path, targetPath); + if (!PathUtils.isAbsolute(targetPath)) { + throw new Exception("absolute path expected: " + targetPath + ", pos: " + pos); + } + } + cb.moveNode(srcPath, targetPath); + break; + } + case '*': { + pos = t.getLastPos(); + String subPath = t.readString(); + String srcPath = PathUtils.concat(path, subPath); + if (!PathUtils.isAbsolute(srcPath)) { + throw new Exception("absolute path expected: " + srcPath + ", pos: " + pos); + } + t.read(':'); + pos = t.getLastPos(); + String targetPath = t.readString(); + if (!PathUtils.isAbsolute(targetPath)) { + targetPath = PathUtils.concat(path, targetPath); + if (!PathUtils.isAbsolute(targetPath)) { + throw new Exception("absolute path expected: " + targetPath + ", pos: " + pos); + } + } + cb.copyNode(srcPath, targetPath); + break; + } + default: + throw new AssertionError("token type: " + t.getTokenType()); + } + } + Id newHead = cb.doCommit(); + if (!newHead.equals(revId)) { + // non-empty commit + if (rep.getCommit(newHead).getBranchRootId() == null) { + // OAK-265: only trigger commit gate for non-branch commits + gate.commit(newHead.toString()); + } + } + return newHead.toString(); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + public String branch(String trunkRevisionId) throws MicroKernelException { + // create a private branch + + if (rep == null) { + throw new IllegalStateException("this instance has already been disposed"); + } + + Id revId = trunkRevisionId == null ? getHeadRevisionId() : Id.fromString(trunkRevisionId); + + try { + CommitBuilder cb = rep.getCommitBuilder(revId, ""); + return cb.doCommit(true).toString(); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + public String merge(String branchRevisionId, String message) throws MicroKernelException { + // merge a private branch with current head revision + + if (rep == null) { + throw new IllegalStateException("this instance has already been disposed"); + } + + Id revId = Id.fromString(branchRevisionId); + + try { + return rep.getCommitBuilder(revId, message).doMerge().toString(); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + public long getLength(String blobId) throws MicroKernelException { + if (rep == null) { + throw new IllegalStateException("this instance has already been disposed"); + } + try { + return rep.getBlobStore().getBlobLength(blobId); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + public int read(String blobId, long pos, byte[] buff, int off, int length) throws MicroKernelException { + if (rep == null) { + throw new IllegalStateException("this instance has already been disposed"); + } + try { + return rep.getBlobStore().readBlob(blobId, pos, buff, off, length); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + public String write(InputStream in) throws MicroKernelException { + if (rep == null) { + throw new IllegalStateException("this instance has already been disposed"); + } + try { + return rep.getBlobStore().writeBlob(in); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + //-------------------------------------------------------< implementation > + + void toJson(JsopBuilder builder, NodeState node, + int depth, int offset, int maxChildNodes, + boolean inclVirtualProps, NodeFilter filter) { + for (PropertyState property : node.getProperties()) { + if (filter == null || filter.includeProperty(property.getName())) { + builder.key(property.getName()).encodedValue(property.getEncodedValue()); + } + } + long childCount = node.getChildNodeCount(); + if (inclVirtualProps) { + if (filter == null || filter.includeProperty(":childNodeCount")) { + // :childNodeCount is by default always included + // unless it is explicitly excluded in the filter + builder.key(":childNodeCount").value(childCount); + } + // check whether :hash has been explicitly included + if (filter != null) { + NameFilter nf = filter.getPropertyFilter(); + if (nf != null + && nf.getInclusionPatterns().contains(":hash") + && !nf.getExclusionPatterns().contains(":hash")) { + builder.key(":hash").value(rep.getRevisionStore().getId(node).toString()); + } + } + } + if (childCount > 0 && depth >= 0) { + if (filter != null) { + NameFilter childFilter = filter.getChildNodeFilter(); + if (childFilter != null && !childFilter.containsWildcard()) { + // optimization for large child node lists: + // no need to iterate over the entire child node list if the filter + // does not include wildcards + int count = maxChildNodes == -1 ? Integer.MAX_VALUE : maxChildNodes; + for (String name : childFilter.getInclusionPatterns()) { + NodeState child = node.getChildNode(name); + if (child != null) { + boolean incl = true; + for (String exclName : childFilter.getExclusionPatterns()) { + if (name.equals(exclName)) { + incl = false; + break; + } + } + if (incl) { + if (count-- <= 0) { + break; + } + builder.key(name).object(); + if (depth > 0) { + toJson(builder, child, depth - 1, 0, maxChildNodes, inclVirtualProps, filter); + } + builder.endObject(); + } + } + } + return; + } + } + + int count = maxChildNodes; + if (count != -1 + && filter != null + && filter.getChildNodeFilter() != null) { + // specific maxChildNodes limit and child node filter + count = -1; + } + int numSiblings = 0; + for (ChildNode entry : node.getChildNodeEntries(offset, count)) { + if (filter == null || filter.includeNode(entry.getName())) { + if (maxChildNodes != -1 && ++numSiblings > maxChildNodes) { + break; + } + builder.key(entry.getName()).object(); + if (depth > 0) { + toJson(builder, entry.getNode(), depth - 1, 0, maxChildNodes, inclVirtualProps, filter); + } + builder.endObject(); + } + } + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/Repository.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/core/Repository.java similarity index 55% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/Repository.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/core/Repository.java index 33f8451f64b..e0e9eebf854 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/Repository.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/core/Repository.java @@ -14,47 +14,73 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk; +package org.apache.jackrabbit.mk.core; -import java.io.Closeable; -import java.io.File; - -import org.apache.jackrabbit.mk.model.ChildNodeEntry; -import org.apache.jackrabbit.mk.model.Commit; +import org.apache.jackrabbit.mk.blobs.BlobStore; +import org.apache.jackrabbit.mk.blobs.FileBlobStore; +import org.apache.jackrabbit.mk.blobs.MemoryBlobStore; import org.apache.jackrabbit.mk.model.CommitBuilder; import org.apache.jackrabbit.mk.model.Id; -import org.apache.jackrabbit.mk.model.Node; import org.apache.jackrabbit.mk.model.StoredCommit; -import org.apache.jackrabbit.mk.model.StoredNode; +import org.apache.jackrabbit.mk.model.tree.NodeState; +import org.apache.jackrabbit.mk.persistence.H2Persistence; +import org.apache.jackrabbit.mk.persistence.InMemPersistence; import org.apache.jackrabbit.mk.store.DefaultRevisionStore; import org.apache.jackrabbit.mk.store.NotFoundException; import org.apache.jackrabbit.mk.store.RevisionStore; import org.apache.jackrabbit.mk.util.IOUtils; -import org.apache.jackrabbit.mk.util.PathUtils; -import org.apache.jackrabbit.oak.model.NodeState; +import org.apache.jackrabbit.oak.commons.PathUtils; + +import java.io.Closeable; +import java.io.File; /** * */ public class Repository { - private final String homeDir; + private final File homeDir; private boolean initialized; private RevisionStore rs; + private BlobStore bs; + private boolean blobStoreNeedsClose; public Repository(String homeDir) throws Exception { File home = new File(homeDir == null ? "." : homeDir, ".mk"); - this.homeDir = home.getCanonicalPath(); + this.homeDir = home.getCanonicalFile(); } /** * Alternate constructor, used for testing. * - * @param rs revision store, already initialized + * @param rs revision store + * @param bs blob store */ - public Repository(RevisionStore rs) { + public Repository(RevisionStore rs, BlobStore bs) { this.homeDir = null; this.rs = rs; + this.bs = bs; + + initialized = true; + } + + /** + * Argument-less constructor, used for in-memory kernel. + */ + protected Repository() { + this.homeDir = null; + + DefaultRevisionStore rs = + new DefaultRevisionStore(new InMemPersistence(), null); + + try { + rs.initialize(); + } catch (Exception e) { + /* Not plausible for in-memory operation */ + throw new InternalError("Unable to initialize in-memory store"); + } + this.rs = rs; + this.bs = new MemoryBlobStore(); initialized = true; } @@ -63,10 +89,22 @@ public void init() throws Exception { if (initialized) { return; } - DefaultRevisionStore rs = new DefaultRevisionStore(); - rs.initialize(new File(homeDir)); - this.rs = rs; + H2Persistence pm = new H2Persistence(); + pm.initialize(homeDir); + + DefaultRevisionStore rs = new DefaultRevisionStore(pm); + rs.initialize(); + + this.rs = rs; + + if (pm instanceof BlobStore) { + bs = (BlobStore) pm; + } else { + bs = new FileBlobStore(new File(homeDir, "blobs").getCanonicalPath()); + blobStoreNeedsClose = true; + } + initialized = true; } @@ -74,6 +112,9 @@ public void shutDown() throws Exception { if (!initialized) { return; } + if (blobStoreNeedsClose && bs instanceof Closeable) { + IOUtils.closeQuietly((Closeable) bs); + } if (rs instanceof Closeable) { IOUtils.closeQuietly((Closeable) rs); } @@ -87,6 +128,14 @@ public RevisionStore getRevisionStore() { return rs; } + + public BlobStore getBlobStore() { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + return bs; + } public Id getHeadRevision() throws Exception { if (!initialized) { @@ -109,96 +158,42 @@ public StoredCommit getCommit(Id id) throws NotFoundException, Exception { return rs.getCommit(id); } - public NodeState getNodeState(Id revId, String path) throws NotFoundException, Exception { - return rs.getNodeState(getNode(revId, path)); - } - - /** - * - * @param revId - * @param path - * @return - * @throws NotFoundException if either path or revision doesn't exist - * @throws Exception if another error occurs - */ - public StoredNode getNode(Id revId, String path) throws NotFoundException, Exception { + public NodeState getNodeState(Id revId, String path) throws Exception { if (!initialized) { throw new IllegalStateException("not initialized"); + } else if (!PathUtils.isAbsolute(path)) { + throw new IllegalArgumentException("illegal path"); } - StoredNode root = rs.getRootNode(revId); - if (PathUtils.denotesRoot(path)) { - return root; + NodeState node = rs.getNodeState(rs.getRootNode(revId)); + for (String name : PathUtils.elements(path)) { + node = node.getChildNode(name); + if (node == null) { + break; + } } - - //return root.getNode(path.substring(1), pm); - Id[] ids = resolvePath(revId, path); - return rs.getNode(ids[ids.length - 1]); + return node; } - public boolean nodeExists(Id revId, String path) { + public boolean nodeExists(Id revId, String path) throws Exception { if (!initialized) { throw new IllegalStateException("not initialized"); - } - - if (!PathUtils.isAbsolute(path)) { + } else if (!PathUtils.isAbsolute(path)) { throw new IllegalArgumentException("illegal path"); } - try { - String[] names = PathUtils.split(path); - Node parent = rs.getRootNode(revId); - for (int i = 0; i < names.length; i++) { - ChildNodeEntry cne = parent.getChildNodeEntry(names[i]); - if (cne == null) { - return false; - } - parent = rs.getNode(cne.getId()); + NodeState node = rs.getNodeState(rs.getRootNode(revId)); + for (String name : PathUtils.elements(path)) { + node = node.getChildNode(name); + if (node == null) { + return false; } - return true; - } catch (Exception e) { - return false; } + return true; } public CommitBuilder getCommitBuilder(Id revId, String msg) throws Exception { return new CommitBuilder(revId, msg, rs); } - /** - * - * @param revId - * @param nodePath - * @return - * @throws IllegalArgumentException if the specified path is not absolute - * @throws NotFoundException if either path or revision doesn't exist - * @throws Exception if another error occurs - */ - Id[] /* array of node id's */ resolvePath(Id revId, String nodePath) throws Exception { - if (!PathUtils.isAbsolute(nodePath)) { - throw new IllegalArgumentException("illegal path"); - } - - Commit commit = rs.getCommit(revId); - - if (PathUtils.denotesRoot(nodePath)) { - return new Id[]{commit.getRootNodeId()}; - } - String[] names = PathUtils.split(nodePath); - Id[] ids = new Id[names.length + 1]; - - // get root node - ids[0] = commit.getRootNodeId(); - Node parent = rs.getNode(ids[0]); - // traverse path and remember id of each element - for (int i = 0; i < names.length; i++) { - ChildNodeEntry cne = parent.getChildNodeEntry(names[i]); - if (cne == null) { - throw new NotFoundException(nodePath); - } - ids[i + 1] = cne.getId(); - parent = rs.getNode(cne.getId()); - } - return ids; - } } diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/htree/ChildNodeEntriesHTree.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/htree/ChildNodeEntriesHTree.java new file mode 100644 index 00000000000..f7fd073111a --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/htree/ChildNodeEntriesHTree.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.htree; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.apache.jackrabbit.mk.model.ChildNodeEntries; +import org.apache.jackrabbit.mk.model.ChildNodeEntry; +import org.apache.jackrabbit.mk.store.Binding; +import org.apache.jackrabbit.mk.store.PersistHook; +import org.apache.jackrabbit.mk.store.RevisionProvider; +import org.apache.jackrabbit.mk.store.RevisionStore; +import org.apache.jackrabbit.mk.store.RevisionStore.PutToken; +import org.apache.jackrabbit.mk.util.AbstractFilteringIterator; +import org.apache.jackrabbit.mk.util.AbstractRangeIterator; + +/** + * HTree based implementation to manage child node entries. + */ +public class ChildNodeEntriesHTree implements ChildNodeEntries, PersistHook { + + private HashDirectory top; + + public ChildNodeEntriesHTree(RevisionProvider provider) { + top = new HashDirectory(provider, 0); + } + + public Object clone() { + ChildNodeEntriesHTree clone = null; + try { + clone = (ChildNodeEntriesHTree) super.clone(); + } catch (CloneNotSupportedException e) { + // can't possibly get here + } + // shallow clone of array of immutable IndexEntry objects + clone.top = (HashDirectory) top.clone(); + return clone; + } + + @Override + public boolean inlined() { + return false; + } + + @Override + public int getCount() { + return top.getCount(); + } + + @Override + public ChildNodeEntry get(String name) { + return top.get(name); + } + + @Override + public Iterator getNames(int offset, int count) { + if (offset < 0 || count < -1) { + throw new IllegalArgumentException(); + } + + if (offset >= getCount() || count == 0) { + List empty = Collections.emptyList(); + return empty.iterator(); + } + + if (count == -1 || (offset + count) > getCount()) { + count = getCount() - offset; + } + + return new AbstractRangeIterator(getEntries(offset, count), 0, -1) { + @Override + protected String doNext() { + ChildNodeEntry cne = (ChildNodeEntry) it.next(); + return cne.getName(); + } + }; + } + + @Override + public Iterator getEntries(int offset, int count) { + return top.getEntries(offset, count); + } + + @Override + public ChildNodeEntry add(ChildNodeEntry entry) { + return top.add(entry); + } + + @Override + public ChildNodeEntry remove(String name) { + return top.remove(name); + } + + @Override + public ChildNodeEntry rename(String oldName, String newName) { + if (oldName.equals(newName)) { + return get(oldName); + } + ChildNodeEntry old = remove(oldName); + if (old == null) { + return null; + } + add(new ChildNodeEntry(newName, old.getId())); + return old; + } + + @Override + public Iterator getAdded(ChildNodeEntries other) { + if (other instanceof ChildNodeEntriesHTree) { + return top.getAdded(((ChildNodeEntriesHTree) other).top); + } + // todo optimize + return new AbstractFilteringIterator(other.getEntries(0, -1)) { + @Override + protected boolean include(ChildNodeEntry entry) { + return get(entry.getName()) == null; + } + }; + } + + @Override + public Iterator getRemoved(ChildNodeEntries other) { + return other.getAdded(this); + } + + @Override + public Iterator getModified(final ChildNodeEntries other) { + if (other instanceof ChildNodeEntriesHTree) { + return top.getModified(((ChildNodeEntriesHTree) other).top); + } + // todo optimize + return new AbstractFilteringIterator(getEntries(0, -1)) { + @Override + protected boolean include(ChildNodeEntry entry) { + ChildNodeEntry namesake = other.get(entry.getName()); + return (namesake != null && !namesake.getId().equals(entry.getId())); + } + }; + } + + @Override + public void serialize(Binding binding) throws Exception { + top.serialize(binding); + } + + public void deserialize(Binding binding) throws Exception { + top.deserialize(binding); + } + + @Override + public void prePersist(RevisionStore store, PutToken token) + throws Exception { + + top.prePersist(store, token); + } + + @Override + public void postPersist(RevisionStore store, PutToken token) + throws Exception { + + // nothing to be done here + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/blobs/MongoBlobStoreTest.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/htree/HashBucket.java similarity index 70% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/blobs/MongoBlobStoreTest.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/htree/HashBucket.java index b56ef4df84b..0aa7557499b 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/blobs/MongoBlobStoreTest.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/htree/HashBucket.java @@ -14,20 +14,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.blobs; +package org.apache.jackrabbit.mk.htree; + +import org.apache.jackrabbit.mk.model.ChildNodeEntriesMap; /** - * Tests the MongoBlobStore implementation. + * Leaf node structure in an HTree. */ -public class MongoBlobStoreTest extends DbBlobStoreTest { - - public void setUp() throws Exception { - store = new MemoryBlobStore(); - // store = new MongoBlobStore(); +class HashBucket extends ChildNodeEntriesMap { + + public HashBucket() { } - - public void testGarbageCollection() throws Exception { - // TODO + + public HashBucket(ChildNodeEntriesMap other) { + super(other); } - } diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/htree/HashDirectory.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/htree/HashDirectory.java new file mode 100644 index 00000000000..ede99df7b2e --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/htree/HashDirectory.java @@ -0,0 +1,654 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.htree; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.apache.jackrabbit.mk.model.ChildNodeEntries; +import org.apache.jackrabbit.mk.model.ChildNodeEntry; +import org.apache.jackrabbit.mk.model.Id; +import org.apache.jackrabbit.mk.store.Binding; +import org.apache.jackrabbit.mk.store.RevisionProvider; +import org.apache.jackrabbit.mk.store.RevisionStore; +import org.apache.jackrabbit.mk.store.RevisionStore.PutToken; + +/** + * Directory structure in an HTree. + */ +class HashDirectory implements ChildNodeEntries { + + private static final List EMPTY = Collections.emptyList(); + private static final int MAX_CHILDREN = 256; + private static final int BIT_SIZE = 8; + private static final int MAX_DEPTH = 3; + + private final RevisionProvider provider; + private final int depth; + private int count; + private IndexEntry[] index = new IndexEntry[MAX_CHILDREN]; + + public HashDirectory(RevisionProvider provider, int depth) { + this.provider = provider; + this.depth = depth; + } + + public HashDirectory(HashDirectory other) { + provider = other.provider; + depth = other.depth; + count = other.count; + index = other.index.clone(); + } + + @Override + public Object clone() { + HashDirectory clone = null; + try { + clone = (HashDirectory) super.clone(); + } catch (CloneNotSupportedException e) { + // can't possibly get here + } + // shallow clone of array of immutable IndexEntry objects + clone.index = index.clone(); + return clone; + } + + public int getCount() { + return count; + } + + public ChildNodeEntry get(String name) { + int hash = hashCode(name); + + IndexEntry ie = index[hash]; + if (ie == null) { + return null; + } + if (ie instanceof ChildNodeEntry) { + ChildNodeEntry cne = (ChildNodeEntry) ie; + return cne.getName().equals(name) ? cne : null; + } + ChildNodeEntries container = ((ContainerEntry) ie).getContainer(); + if (container != null) { + return container.get(name); + } + return null; + } + + public Iterator getEntries(int offset, int count) { + if (offset < 0 || count < -1) { + throw new IllegalArgumentException(); + } + + if (offset >= this.count || count == 0) { + return EMPTY.iterator(); + } + + int skipped = 0; + if (count == -1 || (offset + count) > this.count) { + count = this.count - offset; + } + ArrayList list = new ArrayList(count); + for (IndexEntry ie : index) { + if (ie == null) { + continue; + } + if (skipped + ie.getSize() <= offset) { + skipped += ie.getSize(); + continue; + } + if (ie instanceof ChildNodeEntry) { + list.add((ChildNodeEntry) ie); + } else { + ChildNodeEntries container = ((ContainerEntry) ie).getContainer(); + if (container != null) { + for (Iterator it = container.getEntries(offset - skipped, count - list.size()); + it.hasNext(); ) { + list.add(it.next()); + } + skipped = offset; + } + if (list.size() == count) { + break; + } + } + } + return list.iterator(); + } + + public ChildNodeEntry add(ChildNodeEntry entry) { + int hash = hashCode(entry.getName()); + IndexEntry ie = index[hash]; + if (ie == null) { + index[hash] = new NodeEntry(entry.getName(), entry.getId()); + count++; + return null; + } + if (ie instanceof ChildNodeEntry) { + ChildNodeEntry existing = (ChildNodeEntry) ie; + if (existing.getName().equals(entry.getName())) { + index[hash] = new NodeEntry(entry.getName(), entry.getId()); + return existing; + } else { + ContainerEntry ce = createContainerEntry(); + ce.getContainer().add(existing); + ce.getContainer().add(entry); + index[hash] = ce; + count++; + return null; + } + } + ContainerEntry ce = (ContainerEntry) ie; + ChildNodeEntries container = ce.getContainer(); + ChildNodeEntry existing = container.add(entry); + if (entry.equals(existing)) { + // no-op + return existing; + } + ce.setDirty(container); + if (existing == null) { + // new entry + count++; + } + return existing; + } + + public ChildNodeEntry remove(String name) { + int hash = hashCode(name); + IndexEntry ie = index[hash]; + if (ie == null) { + return null; + } + if (ie instanceof ChildNodeEntry) { + ChildNodeEntry existing = (ChildNodeEntry) ie; + if (existing.getName().equals(name)) { + index[hash] = null; + count--; + return existing; + } else { + return null; + } + } + ContainerEntry ce = (ContainerEntry) ie; + ChildNodeEntries container = ce.getContainer(); + ChildNodeEntry existing = container.remove(name); + if (existing == null) { + return null; + } + if (container.getCount() == 0) { + index[hash] = null; + } else if (container.getCount() == 1) { + // inline single remaining entry + ChildNodeEntry remaining = container.getEntries(0, 1).next(); + index[hash] = new NodeEntry(remaining.getName(), remaining.getId()); + } else { + ce.setDirty(container); + } + count--; + return existing; + } + + public Iterator getAdded(ChildNodeEntries otherContainer) { + if (!(otherContainer instanceof HashDirectory)) { + // needs no implementation + return null; + } + + HashDirectory other = (HashDirectory) otherContainer; + List added = new ArrayList(); + for (int i = 0; i < index.length; i++) { + IndexEntry ie1 = index[i]; + IndexEntry ie2 = other.index[i]; + + if (ie1 == null && ie2 == null || (ie1 != null && ie1.equals(ie2))) { + continue; + } + + // index entries aren't equal + if (ie1 == null) { + // this index entry in null => other must be non-null, add all its entries + if (ie2 instanceof ChildNodeEntry) { + added.add((ChildNodeEntry) ie2); + } else { + ChildNodeEntries container = ((ContainerEntry) ie2).getContainer(); + for (Iterator it = container.getEntries(0, -1); it.hasNext(); ) { + added.add(it.next()); + } + } + continue; + } + + // optimization for simple child node entries + if (ie1 instanceof ChildNodeEntry && ie2 instanceof ChildNodeEntry) { + ChildNodeEntry cne1 = (ChildNodeEntry) ie1; + ChildNodeEntry cne2 = (ChildNodeEntry) ie2; + + if (cne2.getName().equals(cne1.getName())) { + added.add(cne2); + } + continue; + } + + // all other cases + for (Iterator it = ie1.getAdded(ie2); it.hasNext(); ) { + added.add(it.next()); + } + } + return added.iterator(); + } + + public Iterator getModified(ChildNodeEntries otherContainer) { + if (!(otherContainer instanceof HashDirectory)) { + // needs no implementation + return null; + } + + HashDirectory other = (HashDirectory) otherContainer; + List modified = new ArrayList(); + for (int i = 0; i < index.length; i++) { + IndexEntry ie1 = index[i]; + IndexEntry ie2 = other.index[i]; + + if (ie1 == null || ie2 == null || ie1.equals(ie2)) { + continue; + } + + // optimization for simple child node entries + if (ie1 instanceof ChildNodeEntry && ie2 instanceof ChildNodeEntry) { + ChildNodeEntry cne1 = (ChildNodeEntry) ie1; + ChildNodeEntry cne2 = (ChildNodeEntry) ie2; + + if (cne1.getName().equals(cne2.getName()) && !cne1.getId().equals(cne2.getId())) { + modified.add(cne1); + continue; + } + } + + // all other cases + for (Iterator it = ie1.getModified(ie2); it.hasNext();) { + modified.add(it.next()); + } + } + return modified.iterator(); + } + + private ContainerEntry createContainerEntry() { + return depth < MAX_DEPTH - 1 ? new DirectoryEntry(depth + 1) : new BucketEntry(); + } + + private int hashCode(String name) { + int hashMask = hashMask(); + int hash = name.hashCode(); + hash = hash & hashMask; + hash = hash >>> ((MAX_DEPTH - depth) * BIT_SIZE); + hash = hash % MAX_CHILDREN; + return hash; + } + + int hashMask() { + int bits = MAX_CHILDREN-1; + int hashMask = bits << ((MAX_DEPTH - depth) * BIT_SIZE); + return hashMask; + } + + public void deserialize(Binding binding) throws Exception { + count = binding.readIntValue(":count"); + + Binding.StringEntryIterator iter = binding.readStringMap(":index"); + int pos = -1; + while (iter.hasNext()) { + Binding.StringEntry entry = iter.next(); + ++pos; + // deserialize index array entry + assert(pos == Integer.parseInt(entry.getKey())); + if (entry.getValue().length() == 0) { + // "" + index[pos] = null; + } else { + switch (entry.getValue().charAt(0)) { + case 'n': + String value = entry.getValue().substring(1); + int i = value.indexOf(':'); + String id = value.substring(0, i); + String name = value.substring(i + 1); + index[pos] = new NodeEntry(name, Id.fromString(id)); + break; + case 'b': + value = entry.getValue().substring(1); + i = value.indexOf(':'); + id = value.substring(0, i); + int count = Integer.parseInt(value.substring(i + 1)); + index[pos] = new BucketEntry(provider, Id.fromString(id), count); + break; + case 'd': + value = entry.getValue().substring(1); + i = value.indexOf(':'); + id = value.substring(0, i); + count = Integer.parseInt(value.substring(i + 1)); + index[pos] = new DirectoryEntry(provider, Id.fromString(id), count, depth + 1); + break; + } + } + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof HashDirectory) { + HashDirectory other = (HashDirectory) obj; + return Arrays.equals(index, other.index); + } + return false; + } + + public void prePersist(RevisionStore store, PutToken token) + throws Exception { + + for (int i = 0; i < index.length; i++) { + if (index[i] instanceof ContainerEntry) { + ContainerEntry ce = (ContainerEntry) index[i]; + if (ce.isDirty()) { + ce.store(store, token); + } + } + } + } + + public void serialize(Binding binding) throws Exception { + final IndexEntry[] index = this.index; + binding.write(":count", count); + binding.writeMap(":index", index.length, new Binding.StringEntryIterator() { + int pos = -1; + + @Override + public boolean hasNext() { + return pos < index.length - 1; + } + + @Override + public Binding.StringEntry next() { + pos++; + if (pos >= index.length) { + throw new NoSuchElementException(); + } + // serialize index array entry + IndexEntry entry = index[pos]; + if (entry == null) { + // null entry: "" + return new Binding.StringEntry(Integer.toString(pos), ""); + } else { + return new Binding.StringEntry(Integer.toString(pos), entry.toString()); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }); + } + + /** + * Entry inside this a directory's index. + */ + static interface IndexEntry { + + public int getSize(); + + public Iterator getAdded(IndexEntry other); + + public Iterator getModified(IndexEntry other); + } + + /** + * Direct entry inside this a directory's index, pointing to a child node. + */ + static class NodeEntry extends ChildNodeEntry implements IndexEntry { + + public NodeEntry(String name, Id id) { + super(name, id); + } + + public int getSize() { + return 1; + } + + public Iterator getAdded(IndexEntry other) { + if (other == null) { + return null; + } + ChildNodeEntries container = ((ContainerEntry) other).createCompatibleContainer(); + container.add(this); + return container.getAdded(((ContainerEntry) other).getContainer()); + } + + public Iterator getModified(IndexEntry other) { + if (other == null) { + return null; + } + ChildNodeEntries container = ((ContainerEntry) other).createCompatibleContainer(); + container.add(this); + return container.getModified(((ContainerEntry) other).getContainer()); + } + + @Override + public String toString() { + return "n" + getId() + ":" + getName(); + } + } + + /** + * Container entry inside this a directory's index, pointing to either a + * directory or a bucket. + */ + static abstract class ContainerEntry implements IndexEntry { + + protected RevisionProvider provider; + protected Id id; + protected int count; + + protected ChildNodeEntries container; + + public ContainerEntry(RevisionProvider provider, Id id, int count) { + this.provider = provider; + this.id = id; + this.count = count; + } + + public ContainerEntry() { + } + + public abstract ChildNodeEntries getContainer(); + + public abstract ChildNodeEntries createCompatibleContainer(); + + public boolean isDirty() { + return container != null; + } + + public void setDirty(ChildNodeEntries container) { + this.container = container; + } + + public Id getId() { + return id; + } + + public int getSize() { + if (container != null) { + return container.getCount(); + } + return count; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other instanceof ContainerEntry) { + ContainerEntry ce = (ContainerEntry) other; + if (container != null && ce.container != null) { + return container.equals(ce.container); + } + if (container == null && ce.container == null) { + return (count == ce.count && id == null ? ce.id == null : id.equals(ce.id)); + } + } + return false; + } + + public Iterator getAdded(IndexEntry other) { + if (other == null) { + return null; + } + if (other instanceof ChildNodeEntry) { + ChildNodeEntries container = ((ContainerEntry) other).createCompatibleContainer(); + container.add((ChildNodeEntry) other); + return getContainer().getAdded(container); + } + return getContainer().getAdded(((ContainerEntry) other).getContainer()); + } + + public Iterator getModified(IndexEntry other) { + if (other == null) { + return null; + } + if (other instanceof ChildNodeEntry) { + ChildNodeEntries container = ((ContainerEntry) other).createCompatibleContainer(); + container.add((ChildNodeEntry) other); + return getContainer().getModified(container); + } + return getContainer().getModified(((ContainerEntry) other).getContainer()); + } + + public void store(RevisionStore store, PutToken token) throws Exception { + store.putCNEMap(token, container); + } + } + + /** + * Directory entry inside this a directory's index, pointing to a directory on the + * next level. + */ + static class DirectoryEntry extends ContainerEntry { + + private final int depth; + + public DirectoryEntry(RevisionProvider provider, Id id, int count, int depth) { + super(provider, id, count); + + this.depth = depth; + } + + public DirectoryEntry(int depth) { + this.depth = depth; + } + + public ChildNodeEntries getContainer() { + if (container != null) { + return container; + } + + try { + // TODO return provider.getCNEMap(id); + return new HashDirectory(provider, depth); + } catch (Exception e) { + // todo log error and gracefully handle exception + return null; + } + } + + public ChildNodeEntries createCompatibleContainer() { + return new HashDirectory(provider, depth); + } + + @Override + public String toString() { + return "d" + getId() + ":" + getSize(); + } + + public void store(RevisionStore store, PutToken token) throws Exception { + ((HashDirectory) container).prePersist(store, token); + super.store(store, token); + } + } + + /** + * Bucket entry inside this a directory's index, pointing to a bucket or leaf node. + */ + static class BucketEntry extends ContainerEntry { + + public BucketEntry(RevisionProvider provider, Id id, int count) { + super(provider, id, count); + } + + public BucketEntry() { + } + + public HashBucket getContainer() { + if (container != null) { + return (HashBucket) container; + } + + try { + return new HashBucket(provider.getCNEMap(id)); + } catch (Exception e) { + // todo log error and gracefully handle exception + return null; + } + } + + public ChildNodeEntries createCompatibleContainer() { + return new HashBucket(); + } + + @Override + public String toString() { + return "b" + getId() + ":" + getSize(); + } + } + + // ------------------------------------------------------------------------------------------- unimplemented methods + + @Override + public boolean inlined() { + throw new NoSuchMethodError(); + } + + @Override + public Iterator getNames(int offset, int count) { + throw new NoSuchMethodError(); + } + + @Override + public ChildNodeEntry rename(String oldName, String newName) { + throw new NoSuchMethodError(); + } + + @Override + public Iterator getRemoved(ChildNodeEntries other) { + throw new NoSuchMethodError(); + } +} diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/json/JsonObject.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/json/JsonObject.java new file mode 100644 index 00000000000..3da9e7ac443 --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/json/JsonObject.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.mk.json; + +import java.util.HashMap; +import java.util.Map; + +/** + * Simple JSON Object representation + */ +public class JsonObject { + + private Map props = new HashMap(); + private Map children = new HashMap(); + + public static JsonObject create(JsopTokenizer t) { + JsonObject obj = new JsonObject(); + if (!t.matches('}')) { + do { + String key = t.readString(); + t.read(':'); + if (t.matches('{')) { + obj.children.put(key, create(t)); + } else { + obj.props.put(key, t.readRawValue().trim()); + } + } while (t.matches(',')); + t.read('}'); + } + return obj; + } + + public void toJson(JsopBuilder buf) { + toJson(buf, this); + } + + public Map getProperties() { + return props; + } + + public Map getChildren() { + return children; + } + + private static void toJson(JsopBuilder buf, JsonObject obj) { + buf.object(); + for (String name : obj.props.keySet()) { + buf.key(name).encodedValue(obj.props.get(name)); + } + for (String name : obj.children.keySet()) { + buf.key(name); + toJson(buf, obj.children.get(name)); + } + buf.endObject(); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/json/JsopBuilder.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/json/JsopBuilder.java similarity index 95% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/json/JsopBuilder.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/json/JsopBuilder.java index b13bf00054a..f8d74eb7ee8 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/json/JsopBuilder.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/json/JsopBuilder.java @@ -16,8 +16,6 @@ */ package org.apache.jackrabbit.mk.json; -import org.apache.jackrabbit.mk.Constants; - /** * A builder for Json and Jsop strings. It encodes string values, and knows when * a comma is needed. A comma is appended before '{', '[', a value, or a key; @@ -26,6 +24,8 @@ */ public class JsopBuilder implements JsopWriter { + private static final boolean JSON_NEWLINES = false; + private StringBuilder buff = new StringBuilder(); private boolean needComma; private int lineLength, previous; @@ -56,7 +56,7 @@ public JsopBuilder append(JsopWriter buffer) { /** * Append a Jsop tag character. * - * @param string the string to append + * @param tag the string to append * @return this */ public JsopBuilder tag(char tag) { @@ -107,7 +107,7 @@ public JsopBuilder object() { * @return this */ public JsopBuilder endObject() { - if (Constants.JSON_NEWLINES) { + if (JSON_NEWLINES) { buff.append("\n}"); } else { buff.append('}'); @@ -148,7 +148,7 @@ public JsopBuilder endArray() { */ public JsopBuilder key(String name) { optionalCommaAndNewline(name.length()); - if (Constants.JSON_NEWLINES) { + if (JSON_NEWLINES) { buff.append('\n'); } buff.append(encode(name)).append(':'); @@ -336,7 +336,7 @@ public static String prettyPrint(String jsop) { JsopTokenizer t = new JsopTokenizer(jsop); while (true) { prettyPrint(buff, t, " "); - if (t.getTokenType() == JsopTokenizer.END) { + if (t.getTokenType() == JsopReader.END) { return buff.toString(); } } @@ -348,17 +348,17 @@ static String prettyPrint(StringBuilder buff, JsopTokenizer t, String ident) { while (true) { int token = t.read(); switch (token) { - case JsopTokenizer.END: + case JsopReader.END: return buff.toString(); - case JsopTokenizer.STRING: + case JsopReader.STRING: buff.append('\"').append(t.getEscapedToken()).append('\"'); break; - case JsopTokenizer.NUMBER: - case JsopTokenizer.TRUE: - case JsopTokenizer.FALSE: - case JsopTokenizer.NULL: - case JsopTokenizer.IDENTIFIER: - case JsopTokenizer.ERROR: + case JsopReader.NUMBER: + case JsopReader.TRUE: + case JsopReader.FALSE: + case JsopReader.NULL: + case JsopReader.IDENTIFIER: + case JsopReader.ERROR: buff.append(t.getEscapedToken()); break; case '{': diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/json/JsopReader.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/json/JsopReader.java new file mode 100644 index 00000000000..f1f65bb3739 --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/json/JsopReader.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.json; + +import javax.annotation.CheckForNull; + +/** + * A reader for Json and Jsop strings. + */ +public interface JsopReader { + + /** + * The token type that signals the end of the stream. + */ + static final int END = 0; + + /** + * The token type of a string value. + */ + public static final int STRING = 1; + + /** + * The token type of a number value. + */ + public static final int NUMBER = 2; + + /** + * The token type of the value "true". + */ + public static final int TRUE = 3; + + /** + * The token type of the value "false". + */ + public static final int FALSE = 4; + + /** + * The token type of "null". + */ + public static final int NULL = 5; + + /** + * The token type of a parse error. + */ + public static final int ERROR = 6; + + /** + * The token type of an identifier (an unquoted string), if supported by the reader. + */ + public static final int IDENTIFIER = 7; + + /** + * The token type of a comment, if supported by the reader. + */ + public static final int COMMENT = 8; + + /** + * Read a token which must match a given token type. + * + * @param type the token type + * @return the token (null when reading a null value) + * @throws IllegalStateException if the token type doesn't match + */ + String read(int type); + + /** + * Read a string. + * + * @return the de-escaped string (null when reading a null value) + * @throws IllegalStateException if the token type doesn't match + */ + @CheckForNull + String readString(); + + /** + * Read a token and return the token type. + * + * @return the token type + */ + int read(); + + /** + * Read a token which must match a given token type. + * + * @param type the token type + * @return true if there was a match + */ + boolean matches(int type); + + /** + * Return the row (escaped) token. + * + * @return the escaped string (null when reading a null value) + */ + @CheckForNull + String readRawValue(); + + /** + * Get the last token value if the the token type was STRING or NUMBER. For + * STRING, the text is decoded; for NUMBER, it is returned as parsed. In all + * other cases the result is undefined. + * + * @return the token + */ + @CheckForNull + String getToken(); + + /** + * Get the token type of the last token. The token type is one of the known + * types (END, STRING, NUMBER,...), or, for Jsop tags such as "+", "-", + * it is the Unicode character code of the tag. + * + * @return the token type + */ + int getTokenType(); + + /** + * Reset the position to 0, so that to restart reading. + */ + void resetReader(); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/json/JsopStream.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/json/JsopStream.java similarity index 83% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/json/JsopStream.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/json/JsopStream.java index 82127d5c51d..49400ed87bc 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/json/JsopStream.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/json/JsopStream.java @@ -33,10 +33,10 @@ public JsopStream append(JsopWriter w) { for (int i = s.pos; i < s.len; i++) { int token = s.tokens[i]; switch (token & 255) { - case JsopTokenizer.STRING: - case JsopTokenizer.NUMBER: - case JsopTokenizer.IDENTIFIER: - case JsopTokenizer.COMMENT: + case JsopReader.STRING: + case JsopReader.NUMBER: + case JsopReader.IDENTIFIER: + case JsopReader.COMMENT: Object o = s.values[token >> 8]; addToken((token & 255) + addValue(o)); break; @@ -89,7 +89,7 @@ public JsopStream array() { public JsopStream encodedValue(String raw) { optionalComma(); - addToken(JsopTokenizer.COMMENT + addValue(raw)); + addToken(JsopReader.COMMENT + addValue(raw)); needComma = true; return this; } @@ -108,7 +108,7 @@ public JsopStream endObject() { public JsopStream key(String key) { optionalComma(); - addToken(JsopTokenizer.STRING + addValue(key)); + addToken(JsopReader.STRING + addValue(key)); addToken(':'); needComma = false; return this; @@ -129,9 +129,9 @@ public JsopStream object() { public JsopStream value(String value) { optionalComma(); if (value == null) { - addToken(JsopTokenizer.NULL); + addToken(JsopReader.NULL); } else { - addToken(JsopTokenizer.STRING + addValue(value)); + addToken(JsopReader.STRING + addValue(value)); } needComma = true; return this; @@ -139,14 +139,14 @@ public JsopStream value(String value) { public JsopStream value(long x) { optionalComma(); - addToken(JsopTokenizer.NUMBER + addValue(Long.valueOf(x))); + addToken(JsopReader.NUMBER + addValue(Long.valueOf(x))); needComma = true; return this; } public JsopStream value(boolean b) { optionalComma(); - addToken(b ? JsopTokenizer.TRUE : JsopTokenizer.FALSE); + addToken(b ? JsopReader.TRUE : JsopReader.FALSE); needComma = true; return this; } @@ -175,16 +175,16 @@ private void optionalComma() { public String getToken() { int x = tokens[lastPos]; switch (x & 255) { - case JsopTokenizer.STRING: - case JsopTokenizer.NUMBER: - case JsopTokenizer.IDENTIFIER: - case JsopTokenizer.COMMENT: + case JsopReader.STRING: + case JsopReader.NUMBER: + case JsopReader.IDENTIFIER: + case JsopReader.COMMENT: return values[x >> 8].toString(); - case JsopTokenizer.TRUE: + case JsopReader.TRUE: return "true"; - case JsopTokenizer.FALSE: + case JsopReader.FALSE: return "false"; - case JsopTokenizer.NULL: + case JsopReader.NULL: return "null"; } return Character.toString((char) (x & 255)); @@ -236,17 +236,17 @@ public String readRawValue() { int x = tokens[pos]; lastPos = pos++; switch (x & 255) { - case JsopTokenizer.COMMENT: - case JsopTokenizer.NUMBER: - case JsopTokenizer.IDENTIFIER: + case JsopReader.COMMENT: + case JsopReader.NUMBER: + case JsopReader.IDENTIFIER: return values[x >> 8].toString(); - case JsopTokenizer.STRING: + case JsopReader.STRING: return JsopBuilder.encode(values[x >> 8].toString()); - case JsopTokenizer.TRUE: + case JsopReader.TRUE: return "true"; - case JsopTokenizer.FALSE: + case JsopReader.FALSE: return "false"; - case JsopTokenizer.NULL: + case JsopReader.NULL: return "null"; case '[': StringBuilder buff = new StringBuilder(); @@ -264,7 +264,7 @@ public String readRawValue() { } public String readString() { - return read(JsopTokenizer.STRING); + return read(JsopReader.STRING); } public String toString() { @@ -284,21 +284,21 @@ public String toString() { case ']': buff.endArray(); break; - case JsopTokenizer.STRING: + case JsopReader.STRING: buff.value(values[x >> 8].toString()); break; - case JsopTokenizer.TRUE: + case JsopReader.TRUE: buff.value(true); break; - case JsopTokenizer.FALSE: + case JsopReader.FALSE: buff.value(false); break; - case JsopTokenizer.NULL: + case JsopReader.NULL: buff.value(null); break; - case JsopTokenizer.IDENTIFIER: - case JsopTokenizer.NUMBER: - case JsopTokenizer.COMMENT: + case JsopReader.IDENTIFIER: + case JsopReader.NUMBER: + case JsopReader.COMMENT: buff.encodedValue(values[x >> 8].toString()); break; default: diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/json/JsopTokenizer.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/json/JsopTokenizer.java similarity index 95% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/json/JsopTokenizer.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/json/JsopTokenizer.java index e22ba7bd018..1277391c5ae 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/json/JsopTokenizer.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/json/JsopTokenizer.java @@ -21,9 +21,6 @@ */ public class JsopTokenizer implements JsopReader { - public static final int END = 0, STRING = 1, NUMBER = 2, TRUE = 3, FALSE = 4, NULL = 5; - public static final int ERROR = 6, IDENTIFIER = 7, COMMENT = 8; - private static final String[] TYPE = { "end", "string", "number", "true", "false", "null", "error" }; @@ -265,10 +262,13 @@ private int readToken() { } String s = jsop.substring(start, pos); if ("null".equals(s)) { + currentToken = null; return NULL; } else if ("true".equals(s)) { + currentToken = s; return TRUE; } else if ("false".equals(s)) { + currentToken = s; return FALSE; } else { currentToken = s; @@ -373,7 +373,7 @@ private static IllegalArgumentException getFormatException(String s, int i) { * Add an asterisk ('[*]') at the given position. This format is used to * show where parsing failed in a statement. * - * @param s the text + * @param s the text * @param index the position * @return the text with asterisk */ @@ -386,13 +386,14 @@ private static String addAsterisk(String s, int index) { } /** - * Read a value and return the raw Json representation. + * Read a value and return the raw Json representation. This includes arrays + * and nested arrays. * * @return the Json representation of the value */ public String readRawValue() { int start = lastPos; - while (jsop.charAt(start) <= ' ') { + while (start < length && jsop.charAt(start) <= ' ') { start++; } skipRawValue(); @@ -402,13 +403,16 @@ public String readRawValue() { private void skipRawValue() { switch (currentType) { case '[': { - read(); int level = 0; while (true) { - if (matches(']') && level-- == 0) { - break; + if (matches(']')) { + if (--level == 0) { + break; + } } else if (matches('[')) { level++; + } else if (matches(JsopReader.END)) { + throw getFormatException(jsop, pos, "value"); } else { read(); } diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/json/JsopWriter.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/json/JsopWriter.java new file mode 100644 index 00000000000..9e68538a8bd --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/json/JsopWriter.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.json; + +/** + * A builder for Json and Json diff strings. It knows when a comma is needed. A + * comma is appended before '{', '[', a value, or a key; but only if the last + * appended token was '}', ']', or a value. There is no limit to the number of + * nesting levels. + */ +public interface JsopWriter { + + /** + * Append '['. A comma is appended first if needed. + * + * @return this + */ + JsopWriter array(); + + /** + * Append '{'. A comma is appended first if needed. + * + * @return this + */ + JsopWriter object(); + + /** + * Append the key (in quotes) plus a colon. A comma is appended first if + * needed. + * + * @param name the key + * @return this + */ + JsopWriter key(String key); + + /** + * Append a string or null. A comma is appended first if needed. + * + * @param value the value + * @return this + */ + JsopWriter value(String value); + + /** + * Append an already encoded value. A comma is appended first if needed. + * + * @param value the value + * @return this + */ + JsopWriter encodedValue(String raw); + + /** + * Append '}'. + * + * @return this + */ + JsopWriter endObject(); + + /** + * Append ']'. + * + * @return this + */ + JsopWriter endArray(); + + /** + * Append a Jsop tag character. + * + * @param tag the string to append + * @return this + */ + JsopWriter tag(char tag); + + /** + * Append all entries of the given buffer. + * + * @param buffer the buffer + * @return this + */ + JsopWriter append(JsopWriter diff); + + /** + * Append a number. A comma is appended first if needed. + * + * @param value the value + * @return this + */ + JsopWriter value(long x); + + /** + * Append the boolean value 'true' or 'false'. A comma is appended first if + * needed. + * + * @param value the value + * @return this + */ + JsopWriter value(boolean b); + + /** + * Append a newline character. + * + * @return this + */ + JsopWriter newline(); + + /** + * Resets this instance, so that all data is discarded. + */ + void resetWriter(); + + /** + * Set the line length, after which a newline is added (to improve + * readability). + * + * @param length the length + */ + void setLineLength(int length); + +} diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/json/package-info.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/json/package-info.java new file mode 100644 index 00000000000..a70b2685fd4 --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/json/package-info.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +@Version("0.1") +@Export(optional = "provide:=true") +package org.apache.jackrabbit.mk.json; + +import aQute.bnd.annotation.Export; +import aQute.bnd.annotation.Version; + diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/AbstractCommit.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/AbstractCommit.java similarity index 62% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/model/AbstractCommit.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/model/AbstractCommit.java index 97f2dea4b98..06c2c919da9 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/AbstractCommit.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/AbstractCommit.java @@ -32,9 +32,15 @@ public abstract class AbstractCommit implements Commit { // commit message protected String msg; + // changes + protected String changes; + // id of parent commit protected Id parentId; + // id of branch root commit + protected Id branchRootId; + protected AbstractCommit() { } @@ -42,7 +48,9 @@ protected AbstractCommit(Commit other) { this.parentId = other.getParentId(); this.rootNodeId = other.getRootNodeId(); this.msg = other.getMsg(); + this.changes = other.getChanges(); this.commitTS = other.getCommitTS(); + this.branchRootId = other.getBranchRootId(); } public Id getParentId() { @@ -61,10 +69,34 @@ public String getMsg() { return msg; } + public String getChanges() { + return changes; + } + + public Id getBranchRootId() { + return branchRootId; + } + public void serialize(Binding binding) throws Exception { binding.write("rootNodeId", rootNodeId.getBytes()); binding.write("commitTS", commitTS); binding.write("msg", msg == null ? "" : msg); + binding.write("changes", changes == null ? "" : changes); binding.write("parentId", parentId == null ? "" : parentId.toString()); + binding.write("branchRootId", branchRootId == null ? "" : branchRootId.toString()); + } + + //-----------------------------------------------------< Object overrides > + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("rootNodeId: '").append(rootNodeId.toString()).append("', "); + sb.append("commitTS: ").append(commitTS).append(", "); + sb.append("msg: '").append(msg == null ? "" : msg).append("', "); + sb.append("changes: '").append(changes == null ? "" : changes).append("', "); + sb.append("parentId: '").append(parentId == null ? "" : parentId.toString()).append("', "); + sb.append("branchRootId: '").append(branchRootId == null ? "" : branchRootId.toString()).append("'"); + return sb.toString(); } } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/AbstractNode.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/AbstractNode.java similarity index 59% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/model/AbstractNode.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/model/AbstractNode.java index 8a1ee780aac..2f491b09063 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/AbstractNode.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/AbstractNode.java @@ -80,6 +80,71 @@ public Iterator getChildNodeEntries(int offset, int count) { return childEntries.getEntries(offset, count); } + public void diff(Node other, NodeDiffHandler handler) { + // compare properties + + Map oldProps = getProperties(); + Map newProps = other.getProperties(); + + for (Map.Entry entry : oldProps.entrySet()) { + String name = entry.getKey(); + String val = oldProps.get(name); + String newVal = newProps.get(name); + if (newVal == null) { + handler.propDeleted(name, val); + } else { + if (!val.equals(newVal)) { + handler.propChanged(name, val, newVal); + } + } + } + for (Map.Entry entry : newProps.entrySet()) { + String name = entry.getKey(); + if (!oldProps.containsKey(name)) { + handler.propAdded(name, entry.getValue()); + } + } + + // compare child node entries + + if (other instanceof AbstractNode) { + // OAK-46: Efficient diffing of large child node lists + + // delegate to ChildNodeEntries implementation + ChildNodeEntries otherEntries = ((AbstractNode) other).childEntries; + for (Iterator it = childEntries.getAdded(otherEntries); it.hasNext(); ) { + handler.childNodeAdded(it.next()); + } + for (Iterator it = childEntries.getRemoved(otherEntries); it.hasNext(); ) { + handler.childNodeDeleted(it.next()); + } + for (Iterator it = childEntries.getModified(otherEntries); it.hasNext(); ) { + ChildNodeEntry old = it.next(); + ChildNodeEntry modified = otherEntries.get(old.getName()); + handler.childNodeChanged(old, modified.getId()); + } + return; + } + + for (Iterator it = getChildNodeEntries(0, -1); it.hasNext(); ) { + ChildNodeEntry child = it.next(); + ChildNodeEntry newChild = other.getChildNodeEntry(child.getName()); + if (newChild == null) { + handler.childNodeDeleted(child); + } else { + if (!child.getId().equals(newChild.getId())) { + handler.childNodeChanged(child, newChild.getId()); + } + } + } + for (Iterator it = other.getChildNodeEntries(0, -1); it.hasNext(); ) { + ChildNodeEntry child = it.next(); + if (getChildNodeEntry(child.getName()) == null) { + handler.childNodeAdded(child); + } + } + } + public void serialize(Binding binding) throws Exception { final Iterator> iter = properties.entrySet().iterator(); binding.writeMap(":props", properties.size(), diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntries.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntries.java similarity index 84% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntries.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntries.java index 8636b20dee7..2d84e612f54 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntries.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntries.java @@ -25,7 +25,7 @@ */ public interface ChildNodeEntries extends Cloneable { - static final int CAPACITY_THRESHOLD = 10000; + static final int CAPACITY_THRESHOLD = 1000; Object clone(); @@ -52,8 +52,8 @@ public interface ChildNodeEntries extends Cloneable { //-------------------------------------------------------------< diff ops > /** - * Returns those entries that exist in other but not in - * this. + * Returns those entries that exist in {@code other} but not in + * {@code this}. * * @param other * @return @@ -61,8 +61,8 @@ public interface ChildNodeEntries extends Cloneable { Iterator getAdded(final ChildNodeEntries other); /** - * Returns those entries that exist in this but not in - * other. + * Returns those entries that exist in {@code this} but not in + * {@code other}. * * @param other * @return @@ -70,8 +70,8 @@ public interface ChildNodeEntries extends Cloneable { Iterator getRemoved(final ChildNodeEntries other); /** - * Returns this instance's entries that have namesakes in - * other but with different ids. + * Returns {@code this} instance's entries that have namesakes in + * {@code other} but with different {@code id}s. * * @param other * @return diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntriesMap.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntriesMap.java similarity index 92% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntriesMap.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntriesMap.java index 29fd6a86918..b0d290b7e7d 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntriesMap.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntriesMap.java @@ -18,11 +18,12 @@ import org.apache.jackrabbit.mk.store.Binding; import org.apache.jackrabbit.mk.util.AbstractFilteringIterator; -import org.apache.jackrabbit.mk.util.EmptyIterator; import org.apache.jackrabbit.mk.util.RangeIterator; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; /** @@ -30,14 +31,14 @@ */ public class ChildNodeEntriesMap implements ChildNodeEntries { - protected static final Iterator EMPTY_ITER = new EmptyIterator(); + protected static final List EMPTY = Collections.emptyList(); protected HashMap entries = new HashMap(); - ChildNodeEntriesMap() { + public ChildNodeEntriesMap() { } - ChildNodeEntriesMap(ChildNodeEntriesMap other) { + public ChildNodeEntriesMap(ChildNodeEntriesMap other) { entries = (HashMap) other.entries.clone(); } @@ -90,7 +91,8 @@ public Iterator getNames(int offset, int count) { return entries.keySet().iterator(); } else { if (offset >= entries.size() || count == 0) { - return new EmptyIterator(); + List empty = Collections.emptyList(); + return empty.iterator(); } if (count == -1 || (offset + count) > entries.size()) { count = entries.size() - offset; @@ -108,7 +110,7 @@ public Iterator getEntries(int offset, int count) { return entries.values().iterator(); } else { if (offset >= entries.size() || count == 0) { - return EMPTY_ITER; + return EMPTY.iterator(); } if (count == -1 || (offset + count) > entries.size()) { count = entries.size() - offset; @@ -156,10 +158,6 @@ public ChildNodeEntry rename(String oldName, String newName) { @Override public Iterator getAdded(final ChildNodeEntries other) { - if (equals(other)) { - return EMPTY_ITER; - } - return new AbstractFilteringIterator(other.getEntries(0, -1)) { @Override protected boolean include(ChildNodeEntry entry) { @@ -170,10 +168,6 @@ protected boolean include(ChildNodeEntry entry) { @Override public Iterator getRemoved(final ChildNodeEntries other) { - if (equals(other)) { - return EMPTY_ITER; - } - return new AbstractFilteringIterator(entries.values().iterator()) { @Override protected boolean include(ChildNodeEntry entry) { @@ -184,9 +178,6 @@ protected boolean include(ChildNodeEntry entry) { @Override public Iterator getModified(final ChildNodeEntries other) { - if (equals(other)) { - return EMPTY_ITER; - } return new AbstractFilteringIterator(getEntries(0, -1)) { @Override protected boolean include(ChildNodeEntry entry) { diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntriesTree.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntriesTree.java similarity index 97% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntriesTree.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntriesTree.java index c9b54d54bfa..9900589f9e4 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntriesTree.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntriesTree.java @@ -21,10 +21,10 @@ import org.apache.jackrabbit.mk.store.RevisionStore; import org.apache.jackrabbit.mk.util.AbstractFilteringIterator; import org.apache.jackrabbit.mk.util.AbstractRangeIterator; -import org.apache.jackrabbit.mk.util.EmptyIterator; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; @@ -34,8 +34,8 @@ */ public class ChildNodeEntriesTree implements ChildNodeEntries { - protected static final Iterator EMPTY_ITER = new EmptyIterator(); - + protected static final List EMPTY = Collections.emptyList(); + protected int count; protected RevisionProvider revProvider; @@ -113,7 +113,8 @@ public Iterator getNames(int offset, int cnt) { } if (offset >= count || cnt == 0) { - return new EmptyIterator(); + List empty = Collections.emptyList(); + return empty.iterator(); } if (cnt == -1 || (offset + cnt) > count) { @@ -136,7 +137,7 @@ public Iterator getEntries(int offset, int cnt) { } if (offset >= count || cnt == 0) { - return EMPTY_ITER; + return EMPTY.iterator(); } int skipped = 0; @@ -289,10 +290,6 @@ public ChildNodeEntry rename(String oldName, String newName) { @Override public Iterator getAdded(final ChildNodeEntries other) { - if (equals(other)) { - return EMPTY_ITER; - } - if (other instanceof ChildNodeEntriesTree) { List added = new ArrayList(); ChildNodeEntriesTree otherEntries = (ChildNodeEntriesTree) other; @@ -304,7 +301,7 @@ public Iterator getAdded(final ChildNodeEntries other) { if (ie1 == null) { // this index entry in null => other must be non-null if (ie2 instanceof NodeInfo) { - added.add((ChildNodeEntry) ie2); + added.add((ChildNodeEntry) ie2); } else if (ie2 instanceof BucketInfo) { BucketInfo bi = (BucketInfo) ie2; ChildNodeEntries bucket = retrieveBucket(bi.getId()); @@ -366,10 +363,6 @@ protected boolean include(ChildNodeEntry entry) { @Override public Iterator getRemoved(final ChildNodeEntries other) { - if (equals(other)) { - return EMPTY_ITER; - } - if (other instanceof ChildNodeEntriesTree) { List removed = new ArrayList(); ChildNodeEntriesTree otherEntries = (ChildNodeEntriesTree) other; @@ -443,10 +436,6 @@ protected boolean include(ChildNodeEntry entry) { @Override public Iterator getModified(final ChildNodeEntries other) { - if (equals(other)) { - return EMPTY_ITER; - } - if (other instanceof ChildNodeEntriesTree) { List modified = new ArrayList(); ChildNodeEntriesTree otherEntries = (ChildNodeEntriesTree) other; @@ -510,12 +499,12 @@ protected boolean include(ChildNodeEntry entry) { //-------------------------------------------------------< implementation > - protected void persistDirtyBuckets(RevisionStore store) throws Exception { + protected void persistDirtyBuckets(RevisionStore store, RevisionStore.PutToken token) throws Exception { for (int i = 0; i < index.length; i++) { if (index[i] instanceof Bucket) { // dirty bucket Bucket bucket = (Bucket) index[i]; - Id id = store.putCNEMap(bucket); + Id id = store.putCNEMap(token, bucket); index[i] = new BucketInfo(id, bucket.getSize()); } } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntry.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntry.java similarity index 100% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntry.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/model/ChildNodeEntry.java diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/Commit.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/Commit.java new file mode 100644 index 00000000000..bbc284ff62e --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/Commit.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.mk.model; + +import org.apache.jackrabbit.mk.store.Binding; + +/** + * + */ +public interface Commit { + + Id getRootNodeId(); + + Id getParentId(); + + long getCommitTS(); + + String getMsg(); + + String getChanges(); + + /** + * Returns {@code null} if this commit does not represent a branch. + *

    + * Otherwise, returns the id of the branch root commit + * (i.e. the public commit that this private branch is based upon). + * + * + * @return the id of the branch root commit or {@code null} if this commit + * does not represent a branch. + */ + Id getBranchRootId(); + + void serialize(Binding binding) throws Exception; +} diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/CommitBuilder.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/CommitBuilder.java new file mode 100644 index 00000000000..3d480c8c918 --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/CommitBuilder.java @@ -0,0 +1,376 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.model; + +import org.apache.jackrabbit.mk.json.JsonObject; +import org.apache.jackrabbit.mk.json.JsopBuilder; +import org.apache.jackrabbit.mk.model.tree.DiffBuilder; +import org.apache.jackrabbit.mk.store.NotFoundException; +import org.apache.jackrabbit.mk.store.RevisionStore; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * + */ +public class CommitBuilder { + + private static final Logger LOG = LoggerFactory.getLogger(CommitBuilder.class); + + /** revision changes are based upon */ + private Id baseRevId; + + private final String msg; + + private final RevisionStore store; + + // staging area + private final StagedNodeTree stagedTree; + + // change log + private final List changeLog = new ArrayList(); + + public CommitBuilder(Id baseRevId, String msg, RevisionStore store) throws Exception { + this.baseRevId = baseRevId; + this.msg = msg; + this.store = store; + stagedTree = new StagedNodeTree(store, baseRevId); + } + + public void addNode(String parentNodePath, String nodeName, JsonObject node) throws Exception { + Change change = new AddNode(parentNodePath, nodeName, node); + change.apply(); + // update change log + changeLog.add(change); + } + + public void removeNode(String nodePath) throws NotFoundException, Exception { + Change change = new RemoveNode(nodePath); + change.apply(); + // update change log + changeLog.add(change); + } + + public void moveNode(String srcPath, String destPath) throws NotFoundException, Exception { + Change change = new MoveNode(srcPath, destPath); + change.apply(); + // update change log + changeLog.add(change); + } + + public void copyNode(String srcPath, String destPath) throws NotFoundException, Exception { + Change change = new CopyNode(srcPath, destPath); + change.apply(); + // update change log + changeLog.add(change); + } + + public void setProperty(String nodePath, String propName, String propValue) throws Exception { + Change change = new SetProperty(nodePath, propName, propValue); + change.apply(); + // update change log + changeLog.add(change); + } + + public Id /* new revId */ doCommit() throws Exception { + return doCommit(false); + } + + public Id /* new revId */ doCommit(boolean createBranch) throws Exception { + if (stagedTree.isEmpty() && !createBranch) { + // nothing to commit + return baseRevId; + } + + StoredCommit baseCommit = store.getCommit(baseRevId); + if (createBranch && baseCommit.getBranchRootId() != null) { + throw new Exception("cannot branch off a private branch"); + } + + boolean privateCommit = createBranch || baseCommit.getBranchRootId() != null; + + if (!privateCommit) { + Id currentHead = store.getHeadCommitId(); + if (!currentHead.equals(baseRevId)) { + // todo gracefully handle certain conflicts (e.g. changes on moved sub-trees, competing deletes etc) + // update base revision to more recent current head + baseRevId = currentHead; + // reset staging area + stagedTree.reset(baseRevId); + // replay change log on new base revision + for (Change change : changeLog) { + change.apply(); + } + } + } + + RevisionStore.PutToken token = store.createPutToken(); + Id rootNodeId = + changeLog.isEmpty() ? baseCommit.getRootNodeId() : stagedTree.persist(token); + + Id newRevId; + + if (!privateCommit) { + store.lockHead(); + try { + Id currentHead = store.getHeadCommitId(); + if (!currentHead.equals(baseRevId)) { + // there's a more recent head revision + // perform a three-way merge + rootNodeId = stagedTree.merge(store.getNode(rootNodeId), currentHead, baseRevId, token); + // update base revision to more recent current head + baseRevId = currentHead; + } + + if (store.getCommit(currentHead).getRootNodeId().equals(rootNodeId)) { + // the commit didn't cause any changes, + // no need to create new commit object/update head revision + return currentHead; + } + // persist new commit + MutableCommit newCommit = new MutableCommit(); + newCommit.setParentId(baseRevId); + newCommit.setCommitTS(System.currentTimeMillis()); + newCommit.setMsg(msg); + StringBuilder diff = new StringBuilder(); + for (Change change : changeLog) { + if (diff.length() > 0) { + diff.append('\n'); + } + diff.append(change.asDiff()); + } + newCommit.setChanges(diff.toString()); + newCommit.setRootNodeId(rootNodeId); + newCommit.setBranchRootId(null); + newRevId = store.putHeadCommit(token, newCommit, null, null); + } finally { + store.unlockHead(); + } + } else { + // private commit/branch + MutableCommit newCommit = new MutableCommit(); + newCommit.setParentId(baseCommit.getId()); + newCommit.setCommitTS(System.currentTimeMillis()); + newCommit.setMsg(msg); + StringBuilder diff = new StringBuilder(); + for (Change change : changeLog) { + if (diff.length() > 0) { + diff.append('\n'); + } + diff.append(change.asDiff()); + } + newCommit.setChanges(diff.toString()); + newCommit.setRootNodeId(rootNodeId); + if (createBranch) { + newCommit.setBranchRootId(baseCommit.getId()); + } else { + newCommit.setBranchRootId(baseCommit.getBranchRootId()); + } + newRevId = store.putCommit(token, newCommit); + } + + // reset instance + stagedTree.reset(newRevId); + changeLog.clear(); + + return newRevId; + } + + public Id /* new revId */ doMerge() throws Exception { + StoredCommit branchCommit = store.getCommit(baseRevId); + Id branchRootId = branchCommit.getBranchRootId(); + if (branchRootId == null) { + throw new Exception("can only merge a private branch commit"); + } + + RevisionStore.PutToken token = store.createPutToken(); + Id rootNodeId = + changeLog.isEmpty() ? branchCommit.getRootNodeId() : stagedTree.persist(token); + + Id newRevId; + + store.lockHead(); + try { + Id currentHead = store.getHeadCommitId(); + + StoredNode ourRoot = store.getNode(rootNodeId); + + rootNodeId = stagedTree.merge(ourRoot, currentHead, branchRootId, token); + + if (store.getCommit(currentHead).getRootNodeId().equals(rootNodeId)) { + // the merge didn't cause any changes, + // no need to create new commit object/update head revision + return currentHead; + } + MutableCommit newCommit = new MutableCommit(); + newCommit.setParentId(currentHead); + newCommit.setCommitTS(System.currentTimeMillis()); + newCommit.setMsg(msg); + // dynamically build diff of merged commit + String diff = new DiffBuilder( + store.getNodeState(store.getRootNode(currentHead)), + store.getNodeState(store.getNode(rootNodeId)), + "/", -1, store, "").build(); + if (diff.isEmpty()) { + LOG.debug("merge of empty branch {} with differing content hashes encountered, ignore and keep current head {}", + baseRevId, currentHead); + return currentHead; + } + newCommit.setChanges(diff); + newCommit.setRootNodeId(rootNodeId); + newCommit.setBranchRootId(null); + newRevId = store.putHeadCommit(token, newCommit, branchRootId, baseRevId); + } finally { + store.unlockHead(); + } + + // reset instance + stagedTree.reset(newRevId); + changeLog.clear(); + + return newRevId; + } + + //--------------------------------------------------------< inner classes > + + abstract class Change { + abstract void apply() throws Exception; + abstract String asDiff(); + } + + class AddNode extends Change { + String parentNodePath; + String nodeName; + JsonObject node; + + AddNode(String parentNodePath, String nodeName, JsonObject node) { + this.parentNodePath = parentNodePath; + this.nodeName = nodeName; + this.node = node; + } + + @Override + void apply() throws Exception { + stagedTree.add(parentNodePath, nodeName, node); + } + + @Override + String asDiff() { + JsopBuilder diff = new JsopBuilder(); + diff.tag('+').key(PathUtils.concat(parentNodePath, nodeName)); + node.toJson(diff); + return diff.toString(); + } + } + + class RemoveNode extends Change { + String nodePath; + + RemoveNode(String nodePath) { + this.nodePath = nodePath; + } + + @Override + void apply() throws Exception { + stagedTree.remove(nodePath); + } + + @Override + String asDiff() { + JsopBuilder diff = new JsopBuilder(); + diff.tag('-').value(nodePath); + return diff.toString(); + } + } + + class MoveNode extends Change { + String srcPath; + String destPath; + + MoveNode(String srcPath, String destPath) { + this.srcPath = srcPath; + this.destPath = destPath; + } + + @Override + void apply() throws Exception { + stagedTree.move(srcPath, destPath); + } + + @Override + String asDiff() { + JsopBuilder diff = new JsopBuilder(); + diff.tag('>').key(srcPath).value(destPath); + return diff.toString(); + } + } + + class CopyNode extends Change { + String srcPath; + String destPath; + + CopyNode(String srcPath, String destPath) { + this.srcPath = srcPath; + this.destPath = destPath; + } + + @Override + void apply() throws Exception { + stagedTree.copy(srcPath, destPath); + } + + @Override + String asDiff() { + JsopBuilder diff = new JsopBuilder(); + diff.tag('*').key(srcPath).value(destPath); + return diff.toString(); + } + } + + class SetProperty extends Change { + String nodePath; + String propName; + String propValue; + + SetProperty(String nodePath, String propName, String propValue) { + this.nodePath = nodePath; + this.propName = propName; + this.propValue = propValue; + } + + @Override + void apply() throws Exception { + stagedTree.setProperty(nodePath, propName, propValue); + } + + @Override + String asDiff() { + JsopBuilder diff = new JsopBuilder(); + diff.tag('^').key(PathUtils.concat(nodePath, propName)); + if (propValue != null) { + diff.encodedValue(propValue); + } else { + diff.value(null); + } + return diff.toString(); + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/Id.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/Id.java similarity index 77% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/model/Id.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/model/Id.java index 41db8074f70..7922d0b3ec7 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/Id.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/Id.java @@ -29,9 +29,9 @@ * or the string representation. *

    * Important Note:

    - * An {@link Id} is considered immutable. The byte[] + * An {@link Id} is considered immutable. The {@code byte[]} * passed to {@link Id#Id(byte[])} must not be reused or modified, the same - * applies for the byte[] returned by {@link Id#getBytes()}. + * applies for the {@code byte[]} returned by {@link Id#getBytes()}. */ public class Id implements Comparable { @@ -39,10 +39,10 @@ public class Id implements Comparable { private final byte[] raw; /** - * Creates a new instance based on the passed byte[]. + * Creates a new instance based on the passed {@code byte[]}. *

    - * The passed byte[] mus not be reused, it's assumed - * to be owned by the new Id instance. + * The passed {@code byte[]} mus not be reused, it's assumed + * to be owned by the new {@code Id} instance. * * @param raw the byte representation */ @@ -52,7 +52,7 @@ public Id(byte[] raw) { } /** - * Creates an Id instance from its + * Creates an {@code Id} instance from its * string representation as returned by {@link #toString()}. *

    * The following condition holds true: @@ -61,13 +61,29 @@ public Id(byte[] raw) { * assert(Id.fromString(someId.toString()).equals(someId)); * * - * @param s a string representation of an Id - * @return an Id instance + * @param s a string representation of an {@code Id} + * @return an {@code Id} instance */ public static Id fromString(String s) { return new Id(StringUtils.convertHexToBytes(s)); } + /** + * Creates an {@code Id} instance from a long. + * + * @param l a long + * @return an {@code Id} instance + */ + public static Id fromLong(long value) { + byte[] raw = new byte[8]; + + for (int i = raw.length - 1; i >= 0 && value != 0; i--) { + raw[i] = (byte) (value & 0xff); + value >>>= 8; + } + return new Id(raw); + } + @Override public int hashCode() { // the hashCode is intentionally not stored @@ -105,7 +121,7 @@ public int compareTo(Id o) { /** * Returns the raw byte representation of this identifier. *

    - * The returned byte[] MUST NOT be modified! + * The returned {@code byte[]} MUST NOT be modified! * * @return the raw byte representation */ diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/MutableCommit.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/MutableCommit.java similarity index 86% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/model/MutableCommit.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/model/MutableCommit.java index 0992938a265..d31c28027d7 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/MutableCommit.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/MutableCommit.java @@ -39,6 +39,8 @@ public MutableCommit(StoredCommit other) { setRootNodeId(other.getRootNodeId()); setCommitTS(other.getCommitTS()); setMsg(other.getMsg()); + setChanges(other.getChanges()); + setBranchRootId(other.getBranchRootId()); this.id = other.getId(); } @@ -57,7 +59,15 @@ public void setCommitTS(long commitTS) { public void setMsg(String msg) { this.msg = msg; } - + + public void setChanges(String changes) { + this.changes = changes; + } + + public void setBranchRootId(Id branchRootId) { + this.branchRootId = branchRootId; + } + /** * Return the commit id. * diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/MutableNode.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/MutableNode.java similarity index 91% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/model/MutableNode.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/model/MutableNode.java index eac66e161ff..392c6a2cb45 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/MutableNode.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/MutableNode.java @@ -60,15 +60,15 @@ public ChildNodeEntry rename(String oldName, String newName) { //----------------------------------------------------------< PersistHook > @Override - public void prePersist(RevisionStore store) throws Exception { + public void prePersist(RevisionStore store, RevisionStore.PutToken token) throws Exception { if (!childEntries.inlined()) { // persist dirty buckets - ((ChildNodeEntriesTree) childEntries).persistDirtyBuckets(store); + ((ChildNodeEntriesTree) childEntries).persistDirtyBuckets(store, token); } } @Override - public void postPersist(RevisionStore store) throws Exception { + public void postPersist(RevisionStore store, RevisionStore.PutToken token) throws Exception { // there's nothing to do } } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/Node.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/Node.java similarity index 95% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/model/Node.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/model/Node.java index 4f6ae4d57ab..e3da55df125 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/Node.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/Node.java @@ -17,7 +17,6 @@ package org.apache.jackrabbit.mk.model; import org.apache.jackrabbit.mk.store.Binding; -import org.apache.jackrabbit.mk.store.NotFoundException; import java.util.Iterator; import java.util.Map; @@ -36,5 +35,7 @@ public interface Node { Iterator getChildNodeEntries(int offset, int count); + void diff(Node other, NodeDiffHandler handler); + void serialize(Binding binding) throws Exception; } diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/NodeDiffHandler.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/NodeDiffHandler.java new file mode 100644 index 00000000000..214e8c809da --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/NodeDiffHandler.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.mk.model; + +/** + * + */ +public interface NodeDiffHandler { + + void propAdded(String propName, String value); + + void propChanged(String propName, String oldValue, String newValue); + + void propDeleted(String propName, String value); + + void childNodeAdded(ChildNodeEntry added); + + void childNodeDeleted(ChildNodeEntry deleted); + + void childNodeChanged(ChildNodeEntry changed, Id newId); +} diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/StagedNodeTree.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/StagedNodeTree.java new file mode 100644 index 00000000000..7afbd005d59 --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/StagedNodeTree.java @@ -0,0 +1,571 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.model; + +import org.apache.jackrabbit.mk.json.JsonObject; +import org.apache.jackrabbit.mk.model.tree.NodeDelta; +import org.apache.jackrabbit.mk.store.NotFoundException; +import org.apache.jackrabbit.mk.store.RevisionStore; +import org.apache.jackrabbit.oak.commons.PathUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A {@code StagedNodeTree} provides methods to manipulate a specific revision + * of the tree. The changes are recorded and can be persisted by calling + * {@link #persist(RevisionStore.PutToken)}. + */ +public class StagedNodeTree { + + private final RevisionStore store; + + private StagedNode root; + private Id baseRevisionId; + + /** + * Creates a new {@code StagedNodeTree} instance. + * + * @param store revision store used to read from and persist changes + * @param baseRevisionId id of revision the changes should be based upon + */ + public StagedNodeTree(RevisionStore store, Id baseRevisionId) { + this.store = store; + this.baseRevisionId = baseRevisionId; + } + + /** + * Discards all staged changes and resets the base revision to the + * specified new revision id. + * + * @param newBaseRevisionId id of revision the changes should be based upon + */ + public void reset(Id newBaseRevisionId) { + root = null; + baseRevisionId = newBaseRevisionId; + } + + /** + * Returns {@code true} if there are no staged changes, otherwise returns {@code false}. + * + * @return {@code true} if there are no staged changes, otherwise returns {@code false}. + */ + public boolean isEmpty() { + return root == null; + } + + /** + * Persists the staged nodes and returns the {@code Id} of the new root node. + * + * @param token + * @return {@code Id} of new root node or {@code null} if there are no changes to persist. + * @throws Exception if an error occurs + */ + public Id /* new id of root node */ persist(RevisionStore.PutToken token) throws Exception { + return root != null ? root.persist(token) : null; + } + + /** + * Performs a three-way merge merging our tree (rooted at {@code ourRoot}) + * and their tree (identified by {@code newBaseRevisionId}), + * using the common ancestor revision {@code commonAncestorRevisionId} as + * base reference. + *

    + * This instance will be initially reset to {@code newBaseRevisionId}, discarding + * all currently staged changes. + * + * @param ourRoot + * @param newBaseRevisionId + * @param commonAncestorRevisionId + * @param token + * @return {@code Id} of new root node + * @throws Exception + */ + public Id merge(StoredNode ourRoot, + Id newBaseRevisionId, + Id commonAncestorRevisionId, + RevisionStore.PutToken token) throws Exception { + // reset staging area to new base revision + reset(newBaseRevisionId); + + StoredNode baseRoot = store.getRootNode(commonAncestorRevisionId); + StoredNode theirRoot = store.getRootNode(newBaseRevisionId); + + // recursively merge 'our' changes with 'their' changes... + mergeNode(baseRoot, ourRoot, theirRoot, "/"); + + // persist staged nodes + return persist(token); + } + + //-----------------------------------------< tree manipulation operations > + + /** + * Creates a new node named {@code nodeName} at {@code parentNodePath}. + * + * @param parentNodePath parent node path + * @param nodeName name of new node + * @param nodeData {@code JsonObject} representation of the node to be added + * @throws NotFoundException if there's no node at {@code parentNodePath} + * @throws Exception if a node named {@code nodeName} already exists at {@code parentNodePath} + * or if another error occurs + */ + public void add(String parentNodePath, String nodeName, JsonObject nodeData) throws Exception { + StagedNode parent = getStagedNode(parentNodePath, true); + if (parent.getChildNodeEntry(nodeName) != null) { + throw new Exception("there's already a child node with name '" + nodeName + "'"); + } + parent.add(nodeName, nodeData); + } + + /** + * Removes the node at {@code nodePath}. + * + * @param nodePath node path + * @throws Exception + */ + public void remove(String nodePath) throws Exception { + String parentPath = PathUtils.getParentPath(nodePath); + String nodeName = PathUtils.getName(nodePath); + + StagedNode parent = getStagedNode(parentPath, true); + if (parent.remove(nodeName) == null) { + throw new NotFoundException(nodePath); + } + + // discard any staged changes at nodePath + unstageNode(nodePath); + } + + /** + * Creates or updates the property named {@code propName} of the specified node. + *

    + * if {@code propValue == null} the specified property will be removed. + * + * @param nodePath node path + * @param propName property name + * @param propValue property value + * @throws NotFoundException if there's no node at {@code nodePath} + * @throws Exception if another error occurs + */ + public void setProperty(String nodePath, String propName, String propValue) throws Exception { + StagedNode node = getStagedNode(nodePath, true); + + Map properties = node.getProperties(); + if (propValue == null) { + properties.remove(propName); + } else { + properties.put(propName, propValue); + } + } + + /** + * Moves the subtree rooted at {@code srcPath} to {@code destPath}. + * + * @param srcPath path of node to be moved + * @param destPath destination path + * @throws NotFoundException if either the node at {@code srcPath} or the parent + * node of {@code destPath} doesn't exist + * @throws Exception if a node already exists at {@code destPath}, + * if {@code srcPath} denotes an ancestor of {@code destPath} + * or if another error occurs + */ + public void move(String srcPath, String destPath) throws Exception { + if (PathUtils.isAncestor(srcPath, destPath)) { + throw new Exception("target path cannot be descendant of source path: " + destPath); + } + + String srcParentPath = PathUtils.getParentPath(srcPath); + String srcNodeName = PathUtils.getName(srcPath); + + String destParentPath = PathUtils.getParentPath(destPath); + String destNodeName = PathUtils.getName(destPath); + + StagedNode srcParent = getStagedNode(srcParentPath, true); + if (srcParent.getChildNodeEntry(srcNodeName) == null) { + throw new NotFoundException(srcPath); + } + StagedNode destParent = getStagedNode(destParentPath, true); + if (destParent.getChildNodeEntry(destNodeName) != null) { + throw new Exception("node already exists at move destination path: " + destPath); + } + + if (srcParentPath.equals(destParentPath)) { + // rename + srcParent.rename(srcNodeName, destNodeName); + } else { + // move + srcParent.move(srcNodeName, destPath); + } + } + + /** + * Copies the subtree rooted at {@code srcPath} to {@code destPath}. + * + * @param srcPath path of node to be copied + * @param destPath destination path + * @throws NotFoundException if either the node at {@code srcPath} or the parent + * node of {@code destPath} doesn't exist + * @throws Exception if a node already exists at {@code destPath} + * or if another error occurs + */ + public void copy(String srcPath, String destPath) throws Exception { + String srcParentPath = PathUtils.getParentPath(srcPath); + String srcNodeName = PathUtils.getName(srcPath); + + String destParentPath = PathUtils.getParentPath(destPath); + String destNodeName = PathUtils.getName(destPath); + + StagedNode srcParent = getStagedNode(srcParentPath, false); + if (srcParent == null) { + // the subtree to be copied has not been modified + ChildNodeEntry entry = getStoredNode(srcParentPath).getChildNodeEntry(srcNodeName); + if (entry == null) { + throw new NotFoundException(srcPath); + } + StagedNode destParent = getStagedNode(destParentPath, true); + if (destParent.getChildNodeEntry(destNodeName) != null) { + throw new Exception("node already exists at copy destination path: " + destPath); + } + destParent.add(new ChildNodeEntry(destNodeName, entry.getId())); + return; + } + + ChildNodeEntry srcEntry = srcParent.getChildNodeEntry(srcNodeName); + if (srcEntry == null) { + throw new NotFoundException(srcPath); + } + + StagedNode destParent = getStagedNode(destParentPath, true); + StagedNode srcNode = getStagedNode(srcPath, false); + if (srcNode != null) { + // copy the modified subtree + destParent.add(destNodeName, srcNode.copy()); + } else { + destParent.add(new ChildNodeEntry(destNodeName, srcEntry.getId())); + } + } + + //-------------------------------------------------------< implementation > + + /** + * Returns a {@code StagedNode} representation of the specified node. + * If a {@code StagedNode} representation doesn't exist yet a new + * {@code StagedNode} instance will be returned if {@code createIfNotStaged == true}, + * otherwise {@code null} will be returned. + *

    + * A {@code NotFoundException} will be thrown if there's no node at {@code path}. + * + * @param path node path + * @param createIfNotStaged flag controlling whether a new {@code StagedNode} + * instance should be created on demand + * @return a {@code StagedNode} instance or {@code null} if there's no {@code StagedNode} + * representation of the specified node and {@code createIfNotStaged == false} + * @throws NotFoundException if there's no child node with the given name + * @throws Exception if another error occurs + */ + private StagedNode getStagedNode(String path, boolean createIfNotStaged) throws Exception { + assert PathUtils.isAbsolute(path); + + if (root == null) { + if (!createIfNotStaged) { + return null; + } + root = new StagedNode(store.getRootNode(baseRevisionId), store); + } + + if (PathUtils.denotesRoot(path)) { + return root; + } + + StagedNode parent = root, node = null; + for (String name : PathUtils.elements(path)) { + node = parent.getStagedChildNode(name, createIfNotStaged); + if (node == null) { + return null; + } + parent = node; + } + return node; + } + + /** + * Discards all staged changes affecting the subtree rooted at {@code path}. + * + * @param path node path + * @return the discarded {@code StagedNode} representation or {@code null} if there wasn't any + * @throws NotFoundException if there's no node at the specified {@code path} + * @throws Exception if another error occurs + */ + private StagedNode unstageNode(String path) throws Exception { + assert PathUtils.isAbsolute(path); + + if (PathUtils.denotesRoot(path)) { + StagedNode unstaged = root; + root = null; + return unstaged; + } + + String parentPath = PathUtils.getParentPath(path); + String name = PathUtils.getName(path); + + StagedNode parent = getStagedNode(parentPath, false); + if (parent == null) { + return null; + } + + return parent.unstageChildNode(name); + } + + /** + * Returns the {@code StoredNode} at {@code path}. + * + * @param path node path + * @return the {@code StoredNode} at {@code path} + * @throws NotFoundException if there's no node at the specified {@code path} + * @throws Exception if another error occurs + */ + private StoredNode getStoredNode(String path) throws Exception { + assert PathUtils.isAbsolute(path); + + if (PathUtils.denotesRoot(path)) { + return store.getRootNode(baseRevisionId); + } + + StoredNode parent = store.getRootNode(baseRevisionId), node = null; + for (String name : PathUtils.elements(path)) { + ChildNodeEntry entry = parent.getChildNodeEntry(name); + if (entry == null) { + throw new NotFoundException(path); + } + node = store.getNode(entry.getId()); + if (node == null) { + throw new NotFoundException(path); + } + parent = node; + } + return node; + } + + /** + * Performs a three-way merge of the trees rooted at {@code ourRoot}, + * {@code theirRoot}, using the tree at {@code baseRoot} as reference. + */ + private void mergeNode(StoredNode baseNode, StoredNode ourNode, StoredNode theirNode, String path) throws Exception { + NodeDelta theirChanges = new NodeDelta( + store, store.getNodeState(baseNode), store.getNodeState(theirNode)); + NodeDelta ourChanges = new NodeDelta( + store, store.getNodeState(baseNode), store.getNodeState(ourNode)); + + StagedNode stagedNode = getStagedNode(path, true); + + // apply our changes + stagedNode.getProperties().putAll(ourChanges.getAddedProperties()); + stagedNode.getProperties().putAll(ourChanges.getChangedProperties()); + for (String name : ourChanges.getRemovedProperties().keySet()) { + stagedNode.getProperties().remove(name); + } + + for (Map.Entry entry : ourChanges.getAddedChildNodes().entrySet()) { + stagedNode.add(new ChildNodeEntry(entry.getKey(), entry.getValue())); + } + for (Map.Entry entry : ourChanges.getChangedChildNodes().entrySet()) { + if (!theirChanges.getChangedChildNodes().containsKey(entry.getKey())) { + stagedNode.add(new ChildNodeEntry(entry.getKey(), entry.getValue())); + } + } + for (String name : ourChanges.getRemovedChildNodes().keySet()) { + stagedNode.remove(name); + } + + List conflicts = theirChanges.listConflicts(ourChanges); + // resolve/report merge conflicts + for (NodeDelta.Conflict conflict : conflicts) { + String conflictName = conflict.getName(); + String conflictPath = PathUtils.concat(path, conflictName); + switch (conflict.getType()) { + case PROPERTY_VALUE_CONFLICT: + throw new Exception( + "concurrent modification of property " + conflictPath + + " with conflicting values: \"" + + ourNode.getProperties().get(conflictName) + + "\", \"" + + theirNode.getProperties().get(conflictName)); + + case NODE_CONTENT_CONFLICT: { + if (ourChanges.getChangedChildNodes().containsKey(conflictName)) { + // modified subtrees + StoredNode baseChild = store.getNode(baseNode.getChildNodeEntry(conflictName).getId()); + StoredNode ourChild = store.getNode(ourNode.getChildNodeEntry(conflictName).getId()); + StoredNode theirChild = store.getNode(theirNode.getChildNodeEntry(conflictName).getId()); + // merge the dirty subtrees recursively + mergeNode(baseChild, ourChild, theirChild, PathUtils.concat(path, conflictName)); + } else { + // todo handle/merge colliding node creation + throw new Exception("colliding concurrent node creation: " + conflictPath); + } + break; + } + + case REMOVED_DIRTY_PROPERTY_CONFLICT: + stagedNode.getProperties().remove(conflictName); + break; + + case REMOVED_DIRTY_NODE_CONFLICT: + stagedNode.remove(conflictName); + break; + } + + } + } + + //--------------------------------------------------------< inner classes > + + private class StagedNode extends MutableNode { + + private final Map stagedChildNodes = new HashMap(); + + private StagedNode(RevisionStore store) { + super(store); + } + + private StagedNode(Node base, RevisionStore store) { + super(base, store); + } + + /** + * Returns a {@code StagedNode} representation of the specified child node. + * If a {@code StagedNode} representation doesn't exist yet a new + * {@code StagedNode} instance will be returned if {@code createIfNotStaged == true}, + * otherwise {@code null} will be returned. + *

    + * A {@code NotFoundException} will be thrown if there's no child node + * with the given name. + * + * @param name child node name + * @param createIfNotStaged flag controlling whether a new {@code StagedNode} + * instance should be created on demand + * @return a {@code StagedNode} instance or {@code null} if there's no {@code StagedNode} + * representation of the specified child node and {@code createIfNotStaged == false} + * @throws NotFoundException if there's no child node with the given name + * @throws Exception if another error occurs + */ + StagedNode getStagedChildNode(String name, boolean createIfNotStaged) throws Exception { + StagedNode child = stagedChildNodes.get(name); + if (child == null) { + ChildNodeEntry entry = getChildNodeEntry(name); + if (entry != null) { + if (createIfNotStaged) { + child = new StagedNode(store.getNode(entry.getId()), store); + stagedChildNodes.put(name, child); + } + } else { + throw new NotFoundException(name); + } + } + return child; + } + + /** + * Removes the {@code StagedNode} representation of the specified child node if there is one. + * + * @param name child node name + * @return the removed {@code StagedNode} representation or {@code null} if there wasn't any + */ + StagedNode unstageChildNode(String name) { + return stagedChildNodes.remove(name); + } + + StagedNode add(String name, StagedNode node) { + stagedChildNodes.put(name, node); + // child id will be computed on persist + add(new ChildNodeEntry(name, null)); + return node; + } + + StagedNode copy() { + StagedNode copy = new StagedNode(this, store); + // recursively copy staged child nodes + for (Map.Entry entry : stagedChildNodes.entrySet()) { + copy.add(entry.getKey(), entry.getValue().copy()); + } + return copy; + } + + StagedNode add(String name, JsonObject obj) { + StagedNode node = new StagedNode(store); + node.getProperties().putAll(obj.getProperties()); + for (Map.Entry entry : obj.getChildren().entrySet()) { + node.add(entry.getKey(), entry.getValue()); + } + stagedChildNodes.put(name, node); + // child id will be computed on persist + add(new ChildNodeEntry(name, null)); + return node; + } + + void move(String name, String destPath) throws Exception { + ChildNodeEntry srcEntry = getChildNodeEntry(name); + assert srcEntry != null; + + String destParentPath = PathUtils.getParentPath(destPath); + String destName = PathUtils.getName(destPath); + + StagedNode destParent = getStagedNode(destParentPath, true); + + StagedNode target = stagedChildNodes.get(name); + + remove(name); + destParent.add(new ChildNodeEntry(destName, srcEntry.getId())); + + if (target != null) { + // move staged child node + destParent.add(destName, target); + } + } + + @Override + public ChildNodeEntry remove(String name) { + stagedChildNodes.remove(name); + return super.remove(name); + } + + @Override + public ChildNodeEntry rename(String oldName, String newName) { + StagedNode child = stagedChildNodes.remove(oldName); + if (child != null) { + stagedChildNodes.put(newName, child); + } + return super.rename(oldName, newName); + } + + Id persist(RevisionStore.PutToken token) throws Exception { + // recursively persist staged nodes + for (Map.Entry entry : stagedChildNodes.entrySet()) { + String name = entry.getKey(); + StagedNode childNode = entry.getValue(); + // todo decide whether to inline/store child node separately based on some filter criteria + Id id = childNode.persist(token); + // update child node entry + add(new ChildNodeEntry(name, id)); + } + // persist this node + return store.putNode(token, this); + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/StoredCommit.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/StoredCommit.java similarity index 83% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/model/StoredCommit.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/model/StoredCommit.java index 0cb15b75f6d..f191736c393 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/StoredCommit.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/StoredCommit.java @@ -29,17 +29,22 @@ public static StoredCommit deserialize(Id id, Binding binding) throws Exception Id rootNodeId = new Id(binding.readBytesValue("rootNodeId")); long commitTS = binding.readLongValue("commitTS"); String msg = binding.readStringValue("msg"); + String changes = binding.readStringValue("changes"); String parentId = binding.readStringValue("parentId"); + String branchRootId = binding.readStringValue("branchRootId"); return new StoredCommit(id, "".equals(parentId) ? null : Id.fromString(parentId), - commitTS, rootNodeId, "".equals(msg) ? null : msg); + commitTS, rootNodeId, "".equals(msg) ? null : msg, changes, + "".equals(branchRootId) ? null : Id.fromString(branchRootId)); } - public StoredCommit(Id id, Id parentId, long commitTS, Id rootNodeId, String msg) { + public StoredCommit(Id id, Id parentId, long commitTS, Id rootNodeId, String msg, String changes, Id branchRootId) { this.id = id; this.parentId = parentId; this.commitTS = commitTS; this.rootNodeId = rootNodeId; this.msg = msg; + this.changes = changes; + this.branchRootId = branchRootId; } public StoredCommit(Id id, Commit commit) { diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/StoredNode.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/StoredNode.java similarity index 72% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/model/StoredNode.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/model/StoredNode.java index c4d4681f107..b049b60b7a5 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/StoredNode.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/StoredNode.java @@ -31,36 +31,11 @@ public class StoredNode extends AbstractNode { private final Id id; - public static StoredNode deserialize(Id id, RevisionProvider provider, Binding binding) throws Exception { - StoredNode newInstance = new StoredNode(id, provider); - Binding.StringEntryIterator iter = binding.readStringMap(":props"); - while (iter.hasNext()) { - Binding.StringEntry entry = iter.next(); - newInstance.properties.put(entry.getKey(), entry.getValue()); - } - boolean inlined = binding.readIntValue(":inlined") != 0; - if (inlined) { - newInstance.childEntries = ChildNodeEntriesMap.deserialize(binding); - } else { - newInstance.childEntries = ChildNodeEntriesTree.deserialize(provider, binding); - } - return newInstance; - } - - private StoredNode(Id id, RevisionProvider provider) { + public StoredNode(Id id, RevisionProvider provider) { super(provider); this.id = id; } - public StoredNode(Id id, RevisionProvider provider, Map properties, Iterator cneIt) { - super(provider); - this.id = id; - this.properties.putAll(properties); - while (cneIt.hasNext()) { - childEntries.add(cneIt.next()); - } - } - public StoredNode(Id id, Node node, RevisionProvider provider) { super(node, provider); this.id = id; @@ -82,4 +57,17 @@ public Iterator getChildNodeNames(int offset, int count) { return new UnmodifiableIterator(super.getChildNodeNames(offset, count)); } + public void deserialize(Binding binding) throws Exception { + Binding.StringEntryIterator iter = binding.readStringMap(":props"); + while (iter.hasNext()) { + Binding.StringEntry entry = iter.next(); + properties.put(entry.getKey(), entry.getValue()); + } + boolean inlined = binding.readIntValue(":inlined") != 0; + if (inlined) { + childEntries = ChildNodeEntriesMap.deserialize(binding); + } else { + childEntries = ChildNodeEntriesTree.deserialize(provider, binding); + } + } } diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/AbstractChildNode.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/AbstractChildNode.java new file mode 100644 index 00000000000..f2a8f1c671c --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/AbstractChildNode.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.mk.model.tree; + +/** + * Abstract base class for {@link ChildNode} implementations. + * This base class contains default implementations of the + * {@link #equals(Object)} and {@link #hashCode()} methods based on + * the implemented interface. + */ +public abstract class AbstractChildNode implements ChildNode { + + /** + * Checks whether the given object is equal to this one. Two child node + * entries are considered equal if both their names and referenced node + * states match. Subclasses may override this method with a more efficient + * equality check if one is available. + * + * @param that target of the comparison + * @return {@code true} if the objects are equal, + * {@code false} otherwise + */ + @Override + public boolean equals(Object that) { + if (this == that) { + return true; + } else if (that instanceof ChildNode) { + ChildNode other = (ChildNode) that; + return getName().equals(other.getName()) + && getNode().equals(other.getNode()); + } else { + return false; + } + + } + + /** + * Returns a hash code that's compatible with how the + * {@link #equals(Object)} method is implemented. The current + * implementation simply returns the hash code of the child node name + * since {@link ChildNode} instances are not intended for use as + * hash keys. + * + * @return hash code + */ + @Override + public int hashCode() { + return getName().hashCode(); + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/model/AbstractNodeState.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/AbstractNodeState.java similarity index 87% rename from oak-core/src/main/java/org/apache/jackrabbit/oak/model/AbstractNodeState.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/AbstractNodeState.java index 07b54ea4baa..209d04cb4e2 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/model/AbstractNodeState.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/AbstractNodeState.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.oak.model; +package org.apache.jackrabbit.mk.model.tree; /** * Abstract base class for {@link NodeState} implementations. @@ -26,7 +26,7 @@ * the {@link #getProperty(String)} and {@link #getPropertyCount()} methods * based on {@link #getProperties()}. The {@link #getChildNode(String)} and * {@link #getChildNodeCount()} methods are similarly implemented based on - * {@link #getChildNodeEntries(long, long)}. Subclasses should normally + * {@link #getChildNodeEntries(long, int)}. Subclasses should normally * override these method with a more efficient alternatives. */ public abstract class AbstractNodeState implements NodeState { @@ -53,7 +53,7 @@ public long getPropertyCount() { @Override public NodeState getChildNode(String name) { - for (ChildNodeEntry entry : getChildNodeEntries(0, -1)) { + for (ChildNode entry : getChildNodeEntries(0, -1)) { if (name.equals(entry.getName())) { return entry.getNode(); } @@ -65,7 +65,7 @@ public NodeState getChildNode(String name) { @SuppressWarnings("unused") public long getChildNodeCount() { long count = 0; - for (ChildNodeEntry entry : getChildNodeEntries(0, -1)) { + for (ChildNode entry : getChildNodeEntries(0, -1)) { count++; } return count; @@ -78,8 +78,8 @@ public long getChildNodeCount() { * more efficient equality check if one is available. * * @param that target of the comparison - * @return true if the objects are equal, - * false otherwise + * @return {@code true} if the objects are equal, + * {@code false} otherwise */ @Override public boolean equals(Object that) { @@ -103,17 +103,13 @@ public boolean equals(Object that) { } long childNodeCount = 0; - for (ChildNodeEntry entry : getChildNodeEntries(0, -1)) { + for (ChildNode entry : getChildNodeEntries(0, -1)) { if (!entry.getNode().equals(other.getChildNode(entry.getName()))) { return false; } childNodeCount++; } - if (childNodeCount != other.getChildNodeCount()) { - return false; - } - - return true; + return childNodeCount == other.getChildNodeCount(); } /** diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/model/AbstractPropertyState.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/AbstractPropertyState.java similarity index 94% rename from oak-core/src/main/java/org/apache/jackrabbit/oak/model/AbstractPropertyState.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/AbstractPropertyState.java index f7ea4233cda..b6825566a4a 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/model/AbstractPropertyState.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/AbstractPropertyState.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.oak.model; +package org.apache.jackrabbit.mk.model.tree; /** * Abstract base class for {@link PropertyState} implementations. @@ -31,8 +31,7 @@ public abstract class AbstractPropertyState implements PropertyState { * equality check if one is available. * * @param that target of the comparison - * @return true if the objects are equal, - * false otherwise + * @return {@code true} if the objects are equal, {@code false} otherwise */ @Override public boolean equals(Object that) { diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/ChildNode.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/ChildNode.java new file mode 100644 index 00000000000..c483ce9a140 --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/ChildNode.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.mk.model.tree; + +/** + * TODO: document + * + *

    Equality and hash codes

    + *

    + * Two child node entries are considered equal if and only if their names + * and referenced node states match. The {@link Object#equals(Object)} + * method needs to be implemented so that it complies with this definition. + * And while child node entries are not meant for use as hash keys, the + * {@link Object#hashCode()} method should still be implemented according + * to this equality contract. + */ +public interface ChildNode { + + /** + * TODO: document + */ + String getName(); + + /** + * TODO: document + */ + NodeState getNode(); + +} diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/DiffBuilder.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/DiffBuilder.java new file mode 100644 index 00000000000..f023fb37a2f --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/DiffBuilder.java @@ -0,0 +1,280 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.model.tree; + +import org.apache.jackrabbit.mk.json.JsopBuilder; +import org.apache.jackrabbit.oak.commons.PathUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * JSOP Diff Builder + */ +public class DiffBuilder { + + private final NodeState before; + private final NodeState after; + private final String path; + private final int depth; + private final String pathFilter; + private final NodeStore store; + + public DiffBuilder(NodeState before, NodeState after, String path, int depth, + NodeStore store, String pathFilter) { + this.before = before; + this.after = after; + this.path = path; + this.depth = depth; + this.store = store; + this.pathFilter = (pathFilter == null || "".equals(pathFilter)) ? "/" : pathFilter; + } + + public String build() throws Exception { + final JsopBuilder buff = new JsopBuilder(); + // maps (key: id of target node, value: path/to/target) + // for tracking added/removed nodes; this allows us + // to detect 'move' operations + final HashMap addedNodes = new HashMap(); + final HashMap removedNodes = new HashMap(); + + if (!PathUtils.isAncestor(path, pathFilter) + && !path.startsWith(pathFilter)) { + return ""; + } + + if (before == null) { + if (after != null) { + buff.tag('+').key(path).object(); + toJson(buff, after); + return buff.endObject().newline().toString(); + } else { + // path doesn't exist in the specified revisions + return ""; + } + } else if (after == null) { + buff.tag('-'); + buff.value(path); + return buff.newline().toString(); + } + + TraversingNodeDiffHandler diffHandler = new TraversingNodeDiffHandler(store) { + int levels = depth < 0 ? Integer.MAX_VALUE : depth; + @Override + public void propertyAdded(PropertyState after) { + String p = PathUtils.concat(getCurrentPath(), after.getName()); + if (p.startsWith(pathFilter)) { + buff.tag('^'). + key(p). + encodedValue(after.getEncodedValue()). + newline(); + } + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) { + String p = PathUtils.concat(getCurrentPath(), after.getName()); + if (p.startsWith(pathFilter)) { + buff.tag('^'). + key(p). + encodedValue(after.getEncodedValue()). + newline(); + } + } + + @Override + public void propertyDeleted(PropertyState before) { + String p = PathUtils.concat(getCurrentPath(), before.getName()); + if (p.startsWith(pathFilter)) { + // since property and node deletions can't be distinguished + // using the "- " notation we're representing + // property deletions as "^ :null" + buff.tag('^'). + key(p). + value(null). + newline(); + } + } + + @Override + public void childNodeAdded(String name, NodeState after) { + String p = PathUtils.concat(getCurrentPath(), name); + if (p.startsWith(pathFilter)) { + addedNodes.put(after, p); + buff.tag('+'). + key(p).object(); + toJson(buff, after); + buff.endObject().newline(); + } + } + + @Override + public void childNodeDeleted(String name, NodeState before) { + String p = PathUtils.concat(getCurrentPath(), name); + if (p.startsWith(pathFilter)) { + removedNodes.put(before, p); + buff.tag('-'); + buff.value(p); + buff.newline(); + } + } + + @Override + public void childNodeChanged(String name, NodeState before, NodeState after) { + String p = PathUtils.concat(getCurrentPath(), name); + if (PathUtils.isAncestor(p, pathFilter) + || p.startsWith(pathFilter)) { + --levels; + if (levels >= 0) { + // recurse + super.childNodeChanged(name, before, after); + } else { + buff.tag('^'); + buff.key(p); + buff.object().endObject(); + buff.newline(); + } + ++levels; + } + } + }; + diffHandler.start(before, after, path); + + // check if this commit includes 'move' operations + // by building intersection of added and removed nodes + addedNodes.keySet().retainAll(removedNodes.keySet()); + if (!addedNodes.isEmpty()) { + // this commit includes 'move' operations + removedNodes.keySet().retainAll(addedNodes.keySet()); + // addedNodes & removedNodes now only contain information about moved nodes + + // re-build the diff in a 2nd pass, this time representing moves correctly + buff.resetWriter(); + + // TODO refactor code, avoid duplication + + diffHandler = new TraversingNodeDiffHandler(store) { + int levels = depth < 0 ? Integer.MAX_VALUE : depth; + @Override + public void propertyAdded(PropertyState after) { + String p = PathUtils.concat(getCurrentPath(), after.getName()); + if (p.startsWith(pathFilter)) { + buff.tag('^'). + key(p). + encodedValue(after.getEncodedValue()). + newline(); + } + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) { + String p = PathUtils.concat(getCurrentPath(), after.getName()); + if (p.startsWith(pathFilter)) { + buff.tag('^'). + key(p). + encodedValue(after.getEncodedValue()). + newline(); + } + } + + @Override + public void propertyDeleted(PropertyState before) { + String p = PathUtils.concat(getCurrentPath(), before.getName()); + if (p.startsWith(pathFilter)) { + // since property and node deletions can't be distinguished + // using the "- " notation we're representing + // property deletions as "^ :null" + buff.tag('^'). + key(p). + value(null). + newline(); + } + } + + @Override + public void childNodeAdded(String name, NodeState after) { + if (addedNodes.containsKey(after)) { + // moved node, will be processed separately + return; + } + String p = PathUtils.concat(getCurrentPath(), name); + if (p.startsWith(pathFilter)) { + buff.tag('+'). + key(p).object(); + toJson(buff, after); + buff.endObject().newline(); + } + } + + @Override + public void childNodeDeleted(String name, NodeState before) { + if (addedNodes.containsKey(before)) { + // moved node, will be processed separately + return; + } + String p = PathUtils.concat(getCurrentPath(), name); + if (p.startsWith(pathFilter)) { + buff.tag('-'); + buff.value(p); + buff.newline(); + } + } + + @Override + public void childNodeChanged(String name, NodeState before, NodeState after) { + String p = PathUtils.concat(getCurrentPath(), name); + if (PathUtils.isAncestor(p, pathFilter) + || p.startsWith(pathFilter)) { + --levels; + if (levels >= 0) { + // recurse + super.childNodeChanged(name, before, after); + } else { + buff.tag('^'); + buff.value(p); + buff.newline(); + } + ++levels; + } + } + }; + diffHandler.start(before, after, path); + + // finally process moved nodes + for (Map.Entry entry : addedNodes.entrySet()) { + buff.tag('>'). + // path/to/deleted/node + key(removedNodes.get(entry.getKey())). + // path/to/added/node + value(entry.getValue()). + newline(); + } + } + return buff.toString(); + } + + private void toJson(JsopBuilder builder, NodeState node) { + for (PropertyState property : node.getProperties()) { + builder.key(property.getName()).encodedValue(property.getEncodedValue()); + } + for (ChildNode entry : node.getChildNodeEntries(0, -1)) { + builder.key(entry.getName()).object(); + toJson(builder, entry.getNode()); + builder.endObject(); + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/NodeDelta.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/NodeDelta.java similarity index 97% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/model/NodeDelta.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/NodeDelta.java index 18b3d195a1c..188b2ffec6b 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/NodeDelta.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/NodeDelta.java @@ -14,17 +14,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.model; +package org.apache.jackrabbit.mk.model.tree; + +import org.apache.jackrabbit.mk.model.Id; +import org.apache.jackrabbit.mk.store.RevisionProvider; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import org.apache.jackrabbit.mk.store.RevisionProvider; -import org.apache.jackrabbit.oak.model.NodeState; -import org.apache.jackrabbit.oak.model.PropertyState; - /** * */ @@ -67,7 +66,7 @@ public NodeDelta( RevisionProvider provider, NodeState node1, NodeState node2) { this.provider = provider; this.node1 = node1; - new DiffHandler().compare(node1, node2); + provider.compare(node1, node2, new DiffHandler()); } public Map getAddedProperties() { @@ -175,7 +174,7 @@ public List listConflicts(NodeDelta other) { //--------------------------------------------------------< inner classes > - private class DiffHandler extends NodeStateDiff { + private class DiffHandler implements NodeStateDiff { @Override public void propertyAdded(PropertyState after) { diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/model/NodeState.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/NodeState.java similarity index 91% rename from oak-core/src/main/java/org/apache/jackrabbit/oak/model/NodeState.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/NodeState.java index 68845df4a51..9348adf957b 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/model/NodeState.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/NodeState.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.oak.model; +package org.apache.jackrabbit.mk.model.tree; /** * A content tree consists of nodes and properties, each of which @@ -94,12 +94,12 @@ public interface NodeState { * is not parsed or otherwise interpreted by this method. *

    * The namespace of properties and child nodes is shared, so if - * this method returns a non-null value for a given + * this method returns a non-{@code null} value for a given * name, then {@link #getChildNode(String)} is guaranteed to return - * null for the same name. + * {@code null} for the same name. * * @param name name of the property to return - * @return named property, or null if not found + * @return named property, or {@code null} if not found */ PropertyState getProperty(String name); @@ -118,19 +118,19 @@ public interface NodeState { * * @return properties in some stable order */ - Iterable getProperties(); + Iterable getProperties(); /** * Returns the named child node. The name is an opaque string and * is not parsed or otherwise interpreted by this method. *

    * The namespace of properties and child nodes is shared, so if - * this method returns a non-null value for a given + * this method returns a non-{@code null} value for a given * name, then {@link #getProperty(String)} is guaranteed to return - * null for the same name. + * {@code null} for the same name. * * @param name name of the child node to return - * @return named child node, or null if not found + * @return named child node, or {@code null} if not found */ NodeState getChildNode(String name); @@ -142,7 +142,7 @@ public interface NodeState { long getChildNodeCount(); /** - * Returns an iterable of the child node entries starting from the + * Returns an Iterable of the child node entries starting from the * given offset. Multiple iterations are guaranteed to return the * child nodes in the same order, but the specific order used is * implementation-dependent and may change across different states @@ -150,10 +150,10 @@ public interface NodeState { * offset is greater than the offset of the last child node entry. * * @param offset zero-based offset of the first entry to return - * @param length maximum number of entries to return, + * @param count maximum number of entries to return, * or -1 for all remaining entries * @return requested child node entries in some stable order */ - Iterable getChildNodeEntries(long offset, long length); + Iterable getChildNodeEntries(long offset, int count); } diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/NodeStateDiff.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/NodeStateDiff.java new file mode 100644 index 00000000000..73531af58ff --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/NodeStateDiff.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.mk.model.tree; + +/** + * Handler of node state differences. + * The {@link NodeStore#compare(NodeState, NodeState, NodeStateDiff)} reports + * detected node state differences by calling methods of a handler instance + * that implements this interface. The compare method will go through all + * properties and child nodes of the two states, calling the relevant + * added, changed or deleted methods where appropriate. Differences in + * the ordering of properties or child nodes do not affect the comparison, + * and the order in which such differences are reported is unspecified. + */ +public interface NodeStateDiff { + + /** + * Called for all added properties. + * + * @param after property state after the change + */ + void propertyAdded(PropertyState after); + + /** + * Called for all changed properties. The names of the given two + * property states are guaranteed to be the same. + * + * @param before property state before the change + * @param after property state after the change + */ + void propertyChanged(PropertyState before, PropertyState after); + + /** + * Called for all deleted properties. + * + * @param before property state before the change + */ + void propertyDeleted(PropertyState before); + + /** + * Called for all added child nodes. + * + * @param name name of the added child node + * @param after child node state after the change + */ + void childNodeAdded(String name, NodeState after); + + /** + * Called for all changed child nodes. + * + * @param name name of the changed child node + * @param before child node state before the change + * @param after child node state after the change + */ + void childNodeChanged(String name, NodeState before, NodeState after); + + /** + * Called for all deleted child nodes. + * + * @param name name of the deleted child node + * @param before child node state before the change + */ + void childNodeDeleted(String name, NodeState before); + +} diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/NodeStore.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/NodeStore.java new file mode 100644 index 00000000000..326f150b076 --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/NodeStore.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.model.tree; + +/** + * Storage abstraction for content trees. At any given point in time + * the stored content tree is rooted at a single immutable node state. + *

    + * This is a low-level interface that doesn't cover functionality like + * merging concurrent changes or rejecting new tree states based on some + * higher-level consistency constraints. + */ +public interface NodeStore { + + /** + * Returns the latest state of the content tree. + * + * @return root node state + */ + NodeState getRoot(); + + /** + * Compares the given two node states. Any found differences are + * reported by calling the relevant added, changed or deleted methods + * of the given handler. + * + * @param before node state before changes + * @param after node state after changes + * @param diff handler of node state differences + */ + void compare(NodeState before, NodeState after, NodeStateDiff diff); + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/model/PropertyState.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/PropertyState.java similarity index 90% rename from oak-core/src/main/java/org/apache/jackrabbit/oak/model/PropertyState.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/PropertyState.java index a9bd8451db1..a461a61a9e7 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/model/PropertyState.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/PropertyState.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.oak.model; +package org.apache.jackrabbit.mk.model.tree; /** * Immutable property state. A property consists of a name and @@ -32,12 +32,12 @@ public interface PropertyState { /** - * TODO: document + * @return the name of this property state */ String getName(); /** - * FIXME: replace with type-specific accessors + * @return the JSON encoded value of this property state. */ String getEncodedValue(); diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/TraversingNodeDiffHandler.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/TraversingNodeDiffHandler.java similarity index 77% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/model/TraversingNodeDiffHandler.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/TraversingNodeDiffHandler.java index 9c47d1ce4f8..9e02574d5f7 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/model/TraversingNodeDiffHandler.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/tree/TraversingNodeDiffHandler.java @@ -14,28 +14,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.model; +package org.apache.jackrabbit.mk.model.tree; -import org.apache.jackrabbit.mk.util.PathUtils; -import org.apache.jackrabbit.oak.model.NodeState; +import org.apache.jackrabbit.oak.commons.PathUtils; import java.util.Stack; /** * */ -public abstract class TraversingNodeDiffHandler extends NodeStateDiff { +public abstract class TraversingNodeDiffHandler implements NodeStateDiff { + + private final NodeStore store; protected Stack paths = new Stack(); - public void start(NodeState before, NodeState after) { - start(before, after, "/"); + public TraversingNodeDiffHandler(NodeStore store) { + this.store = store; } public void start(NodeState before, NodeState after, String path) { paths.clear(); paths.push(path); - compare(before, after); + store.compare(before, after, this); } protected String getCurrentPath() { @@ -46,7 +47,7 @@ protected String getCurrentPath() { public void childNodeChanged( String name, NodeState before, NodeState after) { paths.push(PathUtils.concat(getCurrentPath(), name)); - compare(before, after); + store.compare(before, after, this); paths.pop(); } diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/osgi/MicroKernelService.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/osgi/MicroKernelService.java new file mode 100644 index 00000000000..a9452e4b89c --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/osgi/MicroKernelService.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.mk.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.Property; +import org.apache.felix.scr.annotations.Service; +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +import org.osgi.service.component.ComponentContext; + +@Component +@Service(MicroKernel.class) +public class MicroKernelService extends MicroKernelImpl { + + @Property(description="The unique name of this instance") + public static final String NAME = "name"; + + @Property(description="The home directory (in-memory if not set)") + public static final String HOME_DIR = "homeDir"; + + private String name; + + @Override + public String toString() { + return name; + } + + @Activate + public void activate(ComponentContext context) { + Object homeDir = context.getProperties().get(HOME_DIR); + name = "" + context.getProperties().get(NAME); + if (homeDir != null) { + init(homeDir.toString()); + } + } + + @Deactivate + public void deactivate() { + dispose(); + } + +} diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/persistence/GCPersistence.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/persistence/GCPersistence.java new file mode 100644 index 00000000000..944084acf9a --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/persistence/GCPersistence.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.persistence; + +import org.apache.jackrabbit.mk.model.Commit; +import org.apache.jackrabbit.mk.model.Id; + +/** + * Advanced persistence implementation offering GC support. + *

    + * The persistence implementation must ensure that objects written between {@link #start()} + * and {@link #sweep()} are not swept, in other words, they must be marked implicitely. + */ +public interface GCPersistence extends Persistence { + + /** + * Start a GC cycle. All objects written to the persistence in subsequent calls are + * marked implicitely, i.e. they must be retained on {@link #sweep()}. + */ + void start(); + + /** + * Mark a commit. + * + * @param id + * commit id + * @return {@code true} if the commit was not marked before; + * {@code false} otherwise + * + * @throws Exception if an error occurs + */ + boolean markCommit(Id id) throws Exception; + + /** + * Replace a commit. Introduced to replace dangling parent commits where + * a parent commit might be collected. + * + * @param id + * commit id + * @param + * @return {@code true} if the commit was not marked before; + * {@code false} otherwise + * + * @throws Exception if an error occurs + */ + void replaceCommit(Id id, Commit commit) throws Exception; + + /** + * Mark a node. + * + * @param id + * node id + * @return {@code true} if the node was not marked before; + * {@code false} otherwise + * + * @throws Exception if an error occurs + */ + boolean markNode(Id id) throws Exception; + + /** + * Mark a child node entry map. + * + * @param id + * child node entry map id + * @return {@code true} if the child node entry map was not marked before; + * {@code false} otherwise + * + * @throws Exception if an error occurs + */ + boolean markCNEMap(Id id) throws Exception; + + /** + * Sweep all objects that are not marked and were written before the GC started. + * + * @return number of swept items or -1 if number is unknown + * @throws Exception if an error occurs + */ + int sweep() throws Exception; +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/persistence/H2Persistence.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/persistence/H2Persistence.java similarity index 59% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/store/persistence/H2Persistence.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/persistence/H2Persistence.java index 7053d9d3fac..ca235c60161 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/persistence/H2Persistence.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/persistence/H2Persistence.java @@ -14,17 +14,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.store.persistence; +package org.apache.jackrabbit.mk.persistence; +import org.apache.jackrabbit.mk.model.ChildNodeEntries; import org.apache.jackrabbit.mk.model.ChildNodeEntriesMap; import org.apache.jackrabbit.mk.model.Commit; import org.apache.jackrabbit.mk.model.Id; import org.apache.jackrabbit.mk.model.Node; import org.apache.jackrabbit.mk.model.StoredCommit; +import org.apache.jackrabbit.mk.model.StoredNode; import org.apache.jackrabbit.mk.store.BinaryBinding; -import org.apache.jackrabbit.mk.store.Binding; import org.apache.jackrabbit.mk.store.IdFactory; import org.apache.jackrabbit.mk.store.NotFoundException; +import org.h2.Driver; import org.h2.jdbcx.JdbcConnectionPool; import java.io.ByteArrayInputStream; @@ -34,15 +36,17 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.Statement; +import java.sql.Timestamp; /** * */ -public class H2Persistence implements Persistence { +public class H2Persistence implements GCPersistence { private static final boolean FAST = Boolean.getBoolean("mk.fastDb"); private JdbcConnectionPool cp; + private long gcStart; // TODO: make this configurable private IdFactory idFactory = IdFactory.getDigestFactory(); @@ -55,7 +59,7 @@ public void initialize(File homeDir) throws Exception { dbDir.mkdirs(); } - Class.forName("org.h2.Driver"); + Driver.load(); String url = "jdbc:h2:" + dbDir.getCanonicalPath() + "/revs"; if (FAST) { url += ";log=0;undo_log=0"; @@ -65,7 +69,8 @@ public void initialize(File homeDir) throws Exception { Connection con = cp.getConnection(); try { Statement stmt = con.createStatement(); - stmt.execute("create table if not exists REVS(ID binary primary key, DATA binary)"); + stmt.execute("create table if not exists REVS(ID binary primary key, DATA binary, TIME timestamp)"); + stmt.execute("create table if not exists NODES(ID binary primary key, DATA binary, TIME timestamp)"); stmt.execute("create table if not exists HEAD(ID binary) as select null"); stmt.execute("create sequence if not exists DATASTORE_ID"); /* @@ -82,7 +87,16 @@ public void close() { cp.dispose(); } - public Id readHead() throws Exception { + public Id[] readIds() throws Exception { + Id lastCommitId = null; + Id headId = readHead(); + if (headId != null) { + lastCommitId = readLastCommitId(); + } + return new Id[] { headId, lastCommitId }; + } + + private Id readHead() throws Exception { Connection con = cp.getConnection(); try { PreparedStatement stmt = con.prepareStatement("select * from HEAD"); @@ -98,6 +112,22 @@ public Id readHead() throws Exception { } } + private Id readLastCommitId() throws Exception { + Connection con = cp.getConnection(); + try { + PreparedStatement stmt = con.prepareStatement("select MAX(ID) from REVS"); + ResultSet rs = stmt.executeQuery(); + byte[] rawId = null; + if (rs.next()) { + rawId = rs.getBytes(1); + } + stmt.close(); + return rawId == null ? null : new Id(rawId); + } finally { + con.close(); + } + } + public void writeHead(Id id) throws Exception { Connection con = cp.getConnection(); try { @@ -110,16 +140,17 @@ public void writeHead(Id id) throws Exception { } } - public Binding readNodeBinding(Id id) throws NotFoundException, Exception { + public void readNode(StoredNode node) throws NotFoundException, Exception { + Id id = node.getId(); Connection con = cp.getConnection(); try { - PreparedStatement stmt = con.prepareStatement("select DATA from REVS where ID = ?"); + PreparedStatement stmt = con.prepareStatement("select DATA from NODES where ID = ?"); try { stmt.setBytes(1, id.getBytes()); ResultSet rs = stmt.executeQuery(); if (rs.next()) { ByteArrayInputStream in = new ByteArrayInputStream(rs.getBytes(1)); - return new BinaryBinding(in); + node.deserialize(new BinaryBinding(in)); } else { throw new NotFoundException(id.toString()); } @@ -136,17 +167,19 @@ public Id writeNode(Node node) throws Exception { node.serialize(new BinaryBinding(out)); byte[] bytes = out.toByteArray(); byte[] rawId = idFactory.createContentId(bytes); + Timestamp ts = new Timestamp(System.currentTimeMillis()); //String id = StringUtils.convertBytesToHex(rawId); Connection con = cp.getConnection(); try { PreparedStatement stmt = con .prepareStatement( - "insert into REVS (ID, DATA) select ?, ? where not exists (select 1 from REVS where ID = ?)"); + "insert into NODES (ID, DATA, TIME) select ?, ?, ? where not exists (select 1 from NODES where ID = ?)"); try { stmt.setBytes(1, rawId); stmt.setBytes(2, bytes); - stmt.setBytes(3, rawId); + stmt.setTimestamp(3, ts); + stmt.setBytes(4, rawId); stmt.executeUpdate(); } finally { stmt.close(); @@ -182,16 +215,17 @@ public void writeCommit(Id id, Commit commit) throws Exception { ByteArrayOutputStream out = new ByteArrayOutputStream(); commit.serialize(new BinaryBinding(out)); byte[] bytes = out.toByteArray(); + Timestamp ts = new Timestamp(System.currentTimeMillis()); Connection con = cp.getConnection(); try { PreparedStatement stmt = con .prepareStatement( - "insert into REVS (ID, DATA) select ?, ? where not exists (select 1 from REVS where ID = ?)"); + "insert into REVS (ID, DATA, TIME) select ?, ?, ?"); try { stmt.setBytes(1, id.getBytes()); stmt.setBytes(2, bytes); - stmt.setBytes(3, id.getBytes()); + stmt.setTimestamp(3, ts); stmt.executeUpdate(); } finally { stmt.close(); @@ -201,10 +235,12 @@ public void writeCommit(Id id, Commit commit) throws Exception { } } + private static final NotFoundException NFE = new NotFoundException(); + public ChildNodeEntriesMap readCNEMap(Id id) throws NotFoundException, Exception { Connection con = cp.getConnection(); try { - PreparedStatement stmt = con.prepareStatement("select DATA from REVS where ID = ?"); + PreparedStatement stmt = con.prepareStatement("select DATA from NODES where ID = ?"); try { stmt.setBytes(1, id.getBytes()); ResultSet rs = stmt.executeQuery(); @@ -212,7 +248,7 @@ public ChildNodeEntriesMap readCNEMap(Id id) throws NotFoundException, Exception ByteArrayInputStream in = new ByteArrayInputStream(rs.getBytes(1)); return ChildNodeEntriesMap.deserialize(new BinaryBinding(in)); } else { - throw new NotFoundException(id.toString()); + throw NFE; // new NotFoundException(id.toString()); } } finally { stmt.close(); @@ -222,21 +258,23 @@ public ChildNodeEntriesMap readCNEMap(Id id) throws NotFoundException, Exception } } - public Id writeCNEMap(ChildNodeEntriesMap map) throws Exception { + public Id writeCNEMap(ChildNodeEntries map) throws Exception { ByteArrayOutputStream out = new ByteArrayOutputStream(); map.serialize(new BinaryBinding(out)); byte[] bytes = out.toByteArray(); byte[] rawId = idFactory.createContentId(bytes); + Timestamp ts = new Timestamp(System.currentTimeMillis()); Connection con = cp.getConnection(); try { PreparedStatement stmt = con .prepareStatement( - "insert into REVS (ID, DATA) select ?, ? where not exists (select 1 from REVS where ID = ?)"); + "insert into NODES (ID, DATA, TIME) select ?, ?, ? where not exists (select 1 from NODES where ID = ?)"); try { stmt.setBytes(1, rawId); stmt.setBytes(2, bytes); - stmt.setBytes(3, rawId); + stmt.setTimestamp(3, ts); + stmt.setBytes(4, rawId); stmt.executeUpdate(); } finally { stmt.close(); @@ -246,4 +284,98 @@ public Id writeCNEMap(ChildNodeEntriesMap map) throws Exception { } return new Id(rawId); } + + @Override + public void start() { + gcStart = System.currentTimeMillis(); + } + + @Override + public boolean markCommit(Id id) throws Exception { + return touch("REVS", id, gcStart); + } + + @Override + public void replaceCommit(Id id, Commit commit) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + commit.serialize(new BinaryBinding(out)); + byte[] bytes = out.toByteArray(); + + Connection con = cp.getConnection(); + try { + PreparedStatement stmt = con + .prepareStatement( + "update REVS set DATA = ?, TIME = CURRENT_TIMESTAMP() where ID = ?"); + try { + stmt.setBytes(1, bytes); + stmt.setBytes(2, id.getBytes()); + stmt.executeUpdate(); + } finally { + stmt.close(); + } + } finally { + con.close(); + } + } + + @Override + public boolean markNode(Id id) throws Exception { + return touch("NODES", id, gcStart); + } + + @Override + public boolean markCNEMap(Id id) throws Exception { + return touch("NODES", id, gcStart); + } + + private boolean touch(String table, Id id, long timeMillis) throws Exception { + Timestamp ts = new Timestamp(timeMillis); + + Connection con = cp.getConnection(); + try { + PreparedStatement stmt = con.prepareStatement( + String.format("update %s set TIME = ? where ID = ? and TIME < ?", + table)); + + try { + stmt.setTimestamp(1, ts); + stmt.setBytes(2, id.getBytes()); + stmt.setTimestamp(3, ts); + return stmt.executeUpdate() == 1; + } finally { + stmt.close(); + } + } finally { + con.close(); + } + } + + @Override + public int sweep() throws Exception { + Timestamp ts = new Timestamp(gcStart); + int swept = 0; + + Connection con = cp.getConnection(); + try { + PreparedStatement stmt = con.prepareStatement("delete REVS where TIME < ?"); + try { + stmt.setTimestamp(1, ts); + swept += stmt.executeUpdate(); + } finally { + stmt.close(); + } + + stmt = con.prepareStatement("delete NODES where TIME < ?"); + + try { + stmt.setTimestamp(1, ts); + swept += stmt.executeUpdate(); + } finally { + stmt.close(); + } + } finally { + con.close(); + } + return swept; + } } \ No newline at end of file diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/persistence/InMemPersistence.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/persistence/InMemPersistence.java new file mode 100644 index 00000000000..f8ddafe0063 --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/persistence/InMemPersistence.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.persistence; + +import org.apache.jackrabbit.mk.model.ChildNodeEntries; +import org.apache.jackrabbit.mk.model.ChildNodeEntriesMap; +import org.apache.jackrabbit.mk.model.Commit; +import org.apache.jackrabbit.mk.model.Id; +import org.apache.jackrabbit.mk.model.Node; +import org.apache.jackrabbit.mk.model.StoredCommit; +import org.apache.jackrabbit.mk.model.StoredNode; +import org.apache.jackrabbit.mk.store.BinaryBinding; +import org.apache.jackrabbit.mk.store.IdFactory; +import org.apache.jackrabbit.mk.store.NotFoundException; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * + */ +public class InMemPersistence implements GCPersistence { + + private final Map objects = Collections.synchronizedMap(new HashMap()); + private final Map marked = Collections.synchronizedMap(new HashMap()); + + private long gcStart; + + // TODO: make this configurable + private IdFactory idFactory = IdFactory.getDigestFactory(); + + @Override + public void initialize(File homeDir) { + // nothing to initialize + } + + @Override + public Id[] readIds() throws Exception { + return new Id[2]; + } + + public void writeHead(Id id) { + + } + + public void readNode(StoredNode node) throws NotFoundException, Exception { + Id id = node.getId(); + byte[] bytes = objects.get(id); + if (bytes != null) { + node.deserialize(new BinaryBinding(new ByteArrayInputStream(bytes))); + return; + } + throw new NotFoundException(id.toString()); + } + + public Id writeNode(Node node) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + node.serialize(new BinaryBinding(out)); + byte[] bytes = out.toByteArray(); + Id id = new Id(idFactory.createContentId(bytes)); + + if (!objects.containsKey(id)) { + objects.put(id, bytes); + } + if (gcStart != 0) { + marked.put(id, bytes); + } + return id; + } + + public StoredCommit readCommit(Id id) throws NotFoundException, Exception { + byte[] bytes = objects.get(id); + if (bytes != null) { + return StoredCommit.deserialize(id, new BinaryBinding(new ByteArrayInputStream(bytes))); + } + throw new NotFoundException(id.toString()); + } + + public void writeCommit(Id id, Commit commit) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + commit.serialize(new BinaryBinding(out)); + byte[] bytes = out.toByteArray(); + + if (!objects.containsKey(id)) { + objects.put(id, bytes); + } + if (gcStart != 0) { + marked.put(id, bytes); + } + } + + public ChildNodeEntriesMap readCNEMap(Id id) throws NotFoundException, Exception { + byte[] bytes = objects.get(id); + if (bytes != null) { + return ChildNodeEntriesMap.deserialize(new BinaryBinding(new ByteArrayInputStream(bytes))); + } + throw new NotFoundException(id.toString()); + } + + public Id writeCNEMap(ChildNodeEntries map) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + map.serialize(new BinaryBinding(out)); + byte[] bytes = out.toByteArray(); + Id id = new Id(idFactory.createContentId(bytes)); + + if (!objects.containsKey(id)) { + objects.put(id, bytes); + } + if (gcStart != 0) { + marked.put(id, bytes); + } + return id; + } + + @Override + public void close() { + // nothing to do here + } + + @Override + public void start() { + gcStart = System.currentTimeMillis(); + marked.clear(); + } + + @Override + public boolean markCommit(Id id) throws NotFoundException { + return markObject(id); + } + + @Override + public boolean markNode(Id id) throws NotFoundException { + return markObject(id); + } + + @Override + public boolean markCNEMap(Id id) throws NotFoundException { + return markObject(id); + } + + @Override + public void replaceCommit(Id id, Commit commit) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + commit.serialize(new BinaryBinding(out)); + byte[] bytes = out.toByteArray(); + + objects.put(id, bytes); + marked.put(id, bytes); + } + + private boolean markObject(Id id) throws NotFoundException { + byte[] data = objects.get(id); + if (data != null) { + return marked.put(id, data) == null; + } + throw new NotFoundException(id.toString()); + } + + @Override + public int sweep() { + int count = objects.size(); + + objects.clear(); + objects.putAll(marked); + + gcStart = 0; + return count; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/persistence/Persistence.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/persistence/Persistence.java similarity index 58% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/store/persistence/Persistence.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/persistence/Persistence.java index c544adeeebb..45ca8838001 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/persistence/Persistence.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/persistence/Persistence.java @@ -14,47 +14,63 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.store.persistence; - -import java.io.File; +package org.apache.jackrabbit.mk.persistence; +import org.apache.jackrabbit.mk.model.ChildNodeEntries; import org.apache.jackrabbit.mk.model.ChildNodeEntriesMap; import org.apache.jackrabbit.mk.model.Commit; import org.apache.jackrabbit.mk.model.Id; import org.apache.jackrabbit.mk.model.Node; import org.apache.jackrabbit.mk.model.StoredCommit; -import org.apache.jackrabbit.mk.store.Binding; +import org.apache.jackrabbit.mk.model.StoredNode; import org.apache.jackrabbit.mk.store.NotFoundException; +import java.io.Closeable; +import java.io.File; + /** * Defines the methods exposed by a persistence manager, that stores head * revision id, nodes, child node entries and blobs. - * - * TODO: instead of deserializing objects on their own, return Binding - * instances, such as in #readNodeBinding. */ -public interface Persistence { +public interface Persistence extends Closeable { - void initialize(File homeDir) throws Exception; - - void close(); - - Id readHead() throws Exception; + public void initialize(File homeDir) throws Exception; + + /** + * Return an array of ids, where the first is the head id (as stored + * with {@link #writeHead(Id)}) and the second is the highest commit + * id found or {@code null}. + *

    + * This method is not guaranteed to deliver "live" results, after + * something is written to the storage, so it should better be used + * once after initialization. + * + * @return array of ids + * @throws Exception if an error occurs + */ + Id[] readIds() throws Exception; void writeHead(Id id) throws Exception; - Binding readNodeBinding(Id id) throws NotFoundException, Exception; + /** + * Read a node from storage. + * + * @param node node to read, with id given in {@link StoredNode#getId()} + * @throws NotFoundException if no such node is found + * @throws Exception if some other error occurs + */ + void readNode(StoredNode node) throws NotFoundException, Exception; Id writeNode(Node node) throws Exception; ChildNodeEntriesMap readCNEMap(Id id) throws NotFoundException, Exception; - Id writeCNEMap(ChildNodeEntriesMap map) throws Exception; + Id writeCNEMap(ChildNodeEntries map) throws Exception; StoredCommit readCommit(Id id) throws NotFoundException, Exception; /** - * Persist a commit, with an id that is selected by the caller. + * Persist a commit with an id provided by the caller. * * @param id commit id * @param commit commit diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/AbstractRevisionStore.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/AbstractRevisionStore.java new file mode 100644 index 00000000000..595da7da813 --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/AbstractRevisionStore.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.store; + +import org.apache.jackrabbit.mk.model.Id; +import org.apache.jackrabbit.mk.model.StoredNode; +import org.apache.jackrabbit.mk.model.tree.ChildNode; +import org.apache.jackrabbit.mk.model.tree.NodeState; +import org.apache.jackrabbit.mk.model.tree.NodeStateDiff; +import org.apache.jackrabbit.mk.model.tree.PropertyState; + +import java.util.HashSet; +import java.util.Set; + +/** + * Abstract base class for revision store implementations. + */ +abstract class AbstractRevisionStore implements RevisionStore { + + @Override + public NodeState getNodeState(StoredNode node) { + return new StoredNodeAsState(node, this); + } + + @Override + public Id getId(NodeState node) { + return ((StoredNodeAsState) node).getId(); + } + + @Override + public NodeState getRoot() { + Id id; + try { + id = getHeadCommitId(); + } catch (Exception e) { + throw new RuntimeException( + "Failed to access the head commit identifier", e); + } + + try { + return getNodeState(getRootNode(id)); + } catch (NotFoundException e) { + throw new IllegalStateException( + "Root node not found in revision " + id, e); + } catch (Exception e) { + throw new RuntimeException( + "Failed to access the root node in revision " + id, e); + } + } + + @Override + public void compare(NodeState before, NodeState after, NodeStateDiff diff) { + compareProperties(before, after, diff); + compareChildNodes(before, after, diff); + } + + /** + * Compares the properties of the given two node states. + * + * @param before node state before changes + * @param after node state after changes + * @param diff handler of node state differences + */ + protected void compareProperties( + NodeState before, NodeState after, NodeStateDiff diff) { + Set beforeProperties = new HashSet(); + + for (PropertyState beforeProperty : before.getProperties()) { + String name = beforeProperty.getName(); + PropertyState afterProperty = after.getProperty(name); + if (afterProperty == null) { + diff.propertyDeleted(beforeProperty); + } else { + beforeProperties.add(name); + if (!beforeProperty.equals(afterProperty)) { + diff.propertyChanged(beforeProperty, afterProperty); + } + } + } + + for (PropertyState afterProperty : after.getProperties()) { + if (!beforeProperties.contains(afterProperty.getName())) { + diff.propertyAdded(afterProperty); + } + } + } + + /** + * Compares the child nodes of the given two node states. + *

    + * Disclaimer: very inefficient implementation for large sets of child node entries + * + * @param before node state before changes + * @param after node state after changes + * @param diff handler of node state differences + */ + protected void compareChildNodes( + NodeState before, NodeState after, NodeStateDiff diff) { + Set beforeChildNodes = new HashSet(); + + for (ChildNode beforeCNE : before.getChildNodeEntries(0, -1)) { + String name = beforeCNE.getName(); + NodeState beforeChild = beforeCNE.getNode(); + NodeState afterChild = after.getChildNode(name); + if (afterChild == null) { + diff.childNodeDeleted(name, beforeChild); + } else { + beforeChildNodes.add(name); + if (!beforeChild.equals(afterChild)) { + diff.childNodeChanged(name, beforeChild, afterChild); + } + } + } + + for (ChildNode afterChild : after.getChildNodeEntries(0, -1)) { + String name = afterChild.getName(); + if (!beforeChildNodes.contains(name)) { + diff.childNodeAdded(name, afterChild.getNode()); + } + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/BinaryBinding.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/BinaryBinding.java similarity index 100% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/store/BinaryBinding.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/store/BinaryBinding.java diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/Binding.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/Binding.java similarity index 100% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/store/Binding.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/store/Binding.java diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/DefaultRevisionStore.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/DefaultRevisionStore.java new file mode 100644 index 00000000000..42b50a52c88 --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/DefaultRevisionStore.java @@ -0,0 +1,699 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.store; + +import org.apache.jackrabbit.mk.model.ChildNodeEntries; +import org.apache.jackrabbit.mk.model.ChildNodeEntriesMap; +import org.apache.jackrabbit.mk.model.ChildNodeEntry; +import org.apache.jackrabbit.mk.model.Id; +import org.apache.jackrabbit.mk.model.MutableCommit; +import org.apache.jackrabbit.mk.model.MutableNode; +import org.apache.jackrabbit.mk.model.Node; +import org.apache.jackrabbit.mk.model.NodeDiffHandler; +import org.apache.jackrabbit.mk.model.StoredCommit; +import org.apache.jackrabbit.mk.model.StoredNode; +import org.apache.jackrabbit.mk.model.tree.NodeState; +import org.apache.jackrabbit.mk.model.tree.NodeStateDiff; +import org.apache.jackrabbit.mk.persistence.GCPersistence; +import org.apache.jackrabbit.mk.persistence.Persistence; +import org.apache.jackrabbit.mk.util.IOUtils; +import org.apache.jackrabbit.mk.util.SimpleLRUCache; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Closeable; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.WeakHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Default revision store implementation, passing calls to a {@code Persistence} + * and a {@code BlobStore}, respectively and providing caching. + */ +public class DefaultRevisionStore extends AbstractRevisionStore implements + Closeable { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultRevisionStore.class); + + public static final String CACHE_SIZE = "mk.cacheSize"; + public static final int DEFAULT_CACHE_SIZE = 10000; + + private boolean initialized; + private Id head; + + private final AtomicLong commitCounter = new AtomicLong(); + + private final ReentrantReadWriteLock headLock = new ReentrantReadWriteLock(); + private final Persistence pm; + protected final GCPersistence gcpm; + + /* avoid synthetic accessor */ int initialCacheSize; + /* avoid synthetic accessor */ Map cache; + + /** + * GC run state constants. + */ + private static final int NOT_ACTIVE = 0; + private static final int STARTING = 1; + private static final int MARKING = 2; + private static final int SWEEPING = 3; + + /** + * GC run state. + */ + private final AtomicInteger gcState = new AtomicInteger(); + + private int markedNodes, markedCommits; + + /** + * GC executor. + */ + private ScheduledExecutorService gcExecutor; + + /** + * Active put tokens (Key: token, Value: null). + */ + private final Map putTokens = Collections.synchronizedMap(new WeakHashMap()); + + /** + * Read-write lock for put tokens. + */ + private final ReentrantReadWriteLock tokensLock = new ReentrantReadWriteLock(); + + /** + * Active branches (Key: current branch head, Value: branch root id). + */ + private final TreeMap branches = new TreeMap(); + + public DefaultRevisionStore(Persistence pm) { + this(pm, (pm instanceof GCPersistence) ? (GCPersistence) pm : null); + } + + /** + * Alternative constructor that allows disabling of garbage collection + * for an in-memory test repository. + * + * @param pm persistence manager + * @param gcpm the same persistence manager, or {@code null} for no GC + */ + public DefaultRevisionStore(Persistence pm, GCPersistence gcpm) { + this.pm = pm; + this.gcpm = gcpm; + } + + public void initialize() throws Exception { + if (initialized) { + throw new IllegalStateException("already initialized"); + } + + initialCacheSize = determineInitialCacheSize(); + cache = Collections.synchronizedMap(SimpleLRUCache. newInstance(initialCacheSize)); + + // make sure we've got a HEAD commit + Id[] ids = pm.readIds(); + head = ids[0]; + if (head == null || head.getBytes().length == 0) { + // assume virgin repository + byte[] rawHead = Id.fromLong(commitCounter.incrementAndGet()) + .getBytes(); + head = new Id(rawHead); + + Id rootNodeId = pm.writeNode(new MutableNode(this)); + MutableCommit initialCommit = new MutableCommit(); + initialCommit.setCommitTS(System.currentTimeMillis()); + initialCommit.setRootNodeId(rootNodeId); + pm.writeCommit(head, initialCommit); + pm.writeHead(head); + } else { + Id lastCommitId = head; + if (ids[1] != null && ids[1].compareTo(lastCommitId) > 0) { + lastCommitId = ids[1]; + } + commitCounter.set(Long.parseLong(lastCommitId.toString(), 16)); + } + + if (gcpm != null) { + gcExecutor = Executors.newScheduledThreadPool(1, + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "RevisionStore-GC"); + } + }); + gcExecutor.scheduleWithFixedDelay(new Runnable() { + @Override + public void run() { + if (cache.size() >= initialCacheSize) { + gc(); + } + } + }, 10, 1, TimeUnit.MINUTES); + } + + initialized = true; + } + + public void close() { + verifyInitialized(); + + if (gcExecutor != null) { + gcExecutor.shutdown(); + } + + cache.clear(); + + IOUtils.closeQuietly(pm); + + initialized = false; + } + + protected void verifyInitialized() { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + } + + protected static int determineInitialCacheSize() { + String val = System.getProperty(CACHE_SIZE); + return (val != null) ? Integer.parseInt(val) : DEFAULT_CACHE_SIZE; + } + + // --------------------------------------------------------< RevisionStore > + + /** + * Put token implementation. + */ + static class PutTokenImpl extends PutToken { + + private static int idCounter; + private int id; + private StoredNode lastModifiedNode; + + public PutTokenImpl() { + this.id = ++idCounter; + } + + @Override + public int hashCode() { + return id; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof PutTokenImpl) { + return ((PutTokenImpl) obj).id == id; + } + return super.equals(obj); + } + + public void updateLastModifed(StoredNode lastModifiedNode) { + this.lastModifiedNode = lastModifiedNode; + } + + public StoredNode getLastModified() { + return lastModifiedNode; + } + } + + public RevisionStore.PutToken createPutToken() { + return new PutTokenImpl(); + } + + public Id putNode(PutToken token, MutableNode node) throws Exception { + verifyInitialized(); + + PersistHook callback = null; + if (node instanceof PersistHook) { + callback = (PersistHook) node; + callback.prePersist(this, token); + } + + /* + * Make sure that a GC cycle can not sweep this newly persisted node + * before we have updated our token + */ + tokensLock.readLock().lock(); + + try { + Id id = pm.writeNode(node); + + if (callback != null) { + callback.postPersist(this, token); + } + + StoredNode snode = new StoredNode(id, node, this); + cache.put(id, snode); + + PutTokenImpl pti = (PutTokenImpl) token; + pti.updateLastModifed(snode); + putTokens.put(pti, null); + return id; + + } finally { + tokensLock.readLock().unlock(); + } + } + + public Id putCNEMap(PutToken token, ChildNodeEntries map) + throws Exception { + verifyInitialized(); + + PersistHook callback = null; + if (map instanceof PersistHook) { + callback = (PersistHook) map; + callback.prePersist(this, token); + } + + Id id = pm.writeCNEMap(map); + + if (callback != null) { + callback.postPersist(this, token); + } + + cache.put(id, map); + + return id; + } + + public void lockHead() { + headLock.writeLock().lock(); + } + + public Id putHeadCommit(PutToken token, MutableCommit commit, Id branchRootId, Id branchRevId) + throws Exception { + verifyInitialized(); + if (!headLock.writeLock().isHeldByCurrentThread()) { + throw new IllegalStateException( + "putHeadCommit called without holding write lock."); + } + + if (commit.getBranchRootId() != null) { + // OAK-267 + throw new IllegalStateException("private branch commit [" + commit + "] cannot become HEAD"); + } + + Id id = writeCommit(token, commit); + setHeadCommitId(id); + + putTokens.remove(token); + if (branchRevId != null) { + synchronized (branches) { + branches.remove(branchRevId); + } + } + return id; + } + + public Id putCommit(PutToken token, MutableCommit commit) throws Exception { + verifyInitialized(); + + Id commitId = writeCommit(token, commit); + putTokens.remove(token); + + Id branchRootId = commit.getBranchRootId(); + if (branchRootId != null) { + synchronized (branches) { + Id parentId = commit.getParentId(); + if (!parentId.equals(branchRootId)) { + /* not the first branch commit, replace its head */ + branches.remove(parentId); + } + branches.put(commitId, branchRootId); + } + } + return commitId; + } + + public void unlockHead() { + headLock.writeLock().unlock(); + } + + // -----------------------------------------------------< RevisionProvider > + + public StoredNode getNode(Id id) throws NotFoundException, Exception { + verifyInitialized(); + + StoredNode node = (StoredNode) cache.get(id); + if (node != null) { + return node; + } + + node = new StoredNode(id, this); + pm.readNode(node); + + cache.put(id, node); + + return node; + } + + public ChildNodeEntriesMap getCNEMap(Id id) throws NotFoundException, + Exception { + verifyInitialized(); + + ChildNodeEntriesMap map = (ChildNodeEntriesMap) cache.get(id); + if (map != null) { + return map; + } + + map = pm.readCNEMap(id); + + cache.put(id, map); + + return map; + } + + public StoredCommit getCommit(Id id) throws NotFoundException, Exception { + verifyInitialized(); + + StoredCommit commit = (StoredCommit) cache.get(id); + if (commit != null) { + return commit; + } + + commit = pm.readCommit(id); + cache.put(id, commit); + + return commit; + } + + public StoredNode getRootNode(Id commitId) throws NotFoundException, + Exception { + return getNode(getCommit(commitId).getRootNodeId()); + } + + public StoredCommit getHeadCommit() throws Exception { + return getCommit(getHeadCommitId()); + } + + public Id getHeadCommitId() throws Exception { + verifyInitialized(); + + headLock.readLock().lock(); + try { + return head; + } finally { + headLock.readLock().unlock(); + } + } + + // -------------------------------------------------------< implementation > + + private Id writeCommit(RevisionStore.PutToken token, MutableCommit commit) + throws Exception { + PersistHook callback = null; + if (commit instanceof PersistHook) { + callback = (PersistHook) commit; + callback.prePersist(this, token); + } + + Id id = commit.getId(); + if (id == null) { + id = Id.fromLong(commitCounter.incrementAndGet()); + } + pm.writeCommit(id, commit); + + if (callback != null) { + callback.postPersist(this, token); + } + cache.put(id, new StoredCommit(id, commit)); + return id; + } + + private void setHeadCommitId(Id id) throws Exception { + // non-synchronized since we're called from putHeadCommit + // which requires a write lock + pm.writeHead(id); + head = id; + + long counter = Long.parseLong(id.toString(), 16); + if (counter > commitCounter.get()) { + commitCounter.set(counter); + } + } + + // ------------------------------------------------------------< overrides > + + @Override + public void compare(final NodeState before, final NodeState after, + final NodeStateDiff diff) { + // OAK-46: Efficient diffing of large child node lists + + Node beforeNode = ((StoredNodeAsState) before).unwrap(); + Node afterNode = ((StoredNodeAsState) after).unwrap(); + + beforeNode.diff(afterNode, new NodeDiffHandler() { + @Override + public void propAdded(String propName, String value) { + diff.propertyAdded(after.getProperty(propName)); + } + + @Override + public void propChanged(String propName, String oldValue, + String newValue) { + diff.propertyChanged(before.getProperty(propName), + after.getProperty(propName)); + } + + @Override + public void propDeleted(String propName, String value) { + diff.propertyDeleted(before.getProperty(propName)); + } + + @Override + public void childNodeAdded(ChildNodeEntry added) { + String name = added.getName(); + diff.childNodeAdded(name, after.getChildNode(name)); + } + + @Override + public void childNodeDeleted(ChildNodeEntry deleted) { + String name = deleted.getName(); + diff.childNodeDeleted(name, before.getChildNode(name)); + } + + @Override + public void childNodeChanged(ChildNodeEntry changed, Id newId) { + String name = changed.getName(); + diff.childNodeChanged(name, before.getChildNode(name), + after.getChildNode(name)); + } + }); + } + + // ----------------------------------------------------------------------- + // GC + + /** + * Perform a garbage collection. If a garbage collection cycle is already + * running, this method returns immediately. + */ + public void gc() { + if (gcpm == null || !gcState.compareAndSet(NOT_ACTIVE, STARTING)) { + // already running + return; + } + + LOG.debug("GC started."); + markedCommits = markedNodes = 0; + + try { + markUncommittedNodes(); + Id firstBranchRootId = markBranches(); + if (firstBranchRootId != null) { + LOG.debug("First branch root to be preserved: {}", firstBranchRootId); + } + Id firstCommitId = markCommits(); + LOG.debug("First commit to be preserved: {}", firstCommitId); + + LOG.debug("Marked {} commits, {} nodes.", markedCommits, markedNodes); + + if (firstBranchRootId != null && firstBranchRootId.compareTo(firstCommitId) < 0) { + firstCommitId = firstBranchRootId; + } + /* repair dangling parent commit of first preserved commit */ + StoredCommit commit = getCommit(firstCommitId); + if (commit.getParentId() != null) { + MutableCommit firstCommit = new MutableCommit(commit); + firstCommit.setParentId(null); + gcpm.replaceCommit(firstCommit.getId(), firstCommit); + } + + } catch (Exception e) { + /* unable to perform GC */ + LOG.error("Exception occurred in GC cycle", e); + gcState.set(NOT_ACTIVE); + return; + } + + gcState.set(SWEEPING); + + try { + int swept = gcpm.sweep(); + LOG.debug("GC cycle swept {} items", swept); + cache.clear(); + } catch (Exception e) { + LOG.error("Exception occurred in GC cycle", e); + } finally { + gcState.set(NOT_ACTIVE); + } + + LOG.debug("GC stopped."); + } + + /** + * Mark nodes that have already been put but not committed yet. + * + * @throws Exception + * if an error occurs + */ + private void markUncommittedNodes() throws Exception { + tokensLock.writeLock().lock(); + + try { + gcpm.start(); + gcState.set(MARKING); + + PutTokenImpl[] tokens = putTokens.keySet().toArray(new PutTokenImpl[putTokens.size()]); + for (PutTokenImpl token : tokens) { + markNode(token.getLastModified()); + } + } finally { + tokensLock.writeLock().unlock(); + } + } + + /** + * Mark branches. + * + * @return first branch root id that needs to be preserved, or {@code null} + * @throws Exception + * if an error occurs + */ + @SuppressWarnings("unchecked") + private Id markBranches() throws Exception { + Map tmpBranches; + + synchronized (branches) { + tmpBranches = (Map) branches.clone(); + } + + Id firstBranchRootId = null; + + /* Mark all branch commits */ + for (Entry entry : tmpBranches.entrySet()) { + Id branchRevId = entry.getKey(); + Id branchRootId = entry.getValue(); + while (!branchRevId.equals(branchRootId)) { + StoredCommit commit = getCommit(branchRevId); + markCommit(commit); + branchRevId = commit.getParentId(); + } + if (firstBranchRootId == null || firstBranchRootId.compareTo(branchRootId) > 0) { + firstBranchRootId = branchRootId; + } + } + /* Mark all master commits till the first branch root id */ + if (firstBranchRootId != null) { + StoredCommit commit = getHeadCommit(); + + for (;;) { + markCommit(commit); + if (commit.getId().equals(firstBranchRootId)) { + break; + } + commit = getCommit(commit.getParentId()); + } + return firstBranchRootId; + } + return null; + } + + /** + * Mark all commits and nodes in a garbage collection cycle. Can be + * customized by subclasses. The default implementation preserves all + * commits that were created within 60 minutes of the current head commit. + *

    + * If this method throws an exception, the cycle will be stopped without + * sweeping. + * + * @return first commit id that will be preserved + * @throws Exception + * if an error occurs + */ + protected Id markCommits() throws Exception { + StoredCommit commit = getHeadCommit(); + long tsLimit = commit.getCommitTS() - (60 * 60 * 1000); + + for (;;) { + markCommit(commit); + Id id = commit.getParentId(); + if (id == null) { + break; + } + StoredCommit parentCommit = getCommit(id); + if (parentCommit.getCommitTS() < tsLimit) { + break; + } + commit = parentCommit; + } + return commit.getId(); + } + + /** + * Mark a commit. This marks all nodes belonging to this commit as well. + * + * @param commit commit + * @throws Exception if an error occurs + */ + protected void markCommit(StoredCommit commit) throws Exception { + if (!gcpm.markCommit(commit.getId())) { + return; + } + markedCommits++; + + markNode(getNode(commit.getRootNodeId())); + } + + /** + * Mark a node. This marks all children as well. + * + * @param node node + * @throws Exception if an error occurs + */ + private void markNode(StoredNode node) throws Exception { + if (!gcpm.markNode(node.getId())) { + return; + } + markedNodes++; + + Iterator iter = node.getChildNodeEntries(0, -1); + while (iter.hasNext()) { + ChildNodeEntry c = iter.next(); + markNode(getNode(c.getId())); + } + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/IdFactory.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/IdFactory.java similarity index 92% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/store/IdFactory.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/store/IdFactory.java index 7eb90bd0bb3..560429d437b 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/IdFactory.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/IdFactory.java @@ -26,9 +26,9 @@ public abstract class IdFactory { /** * Creates a new id based on the specified serialized data. *

    - * The general contract of createContentId is: + * The general contract of {@code createContentId} is: *

    - * createId(data1).equals(createId(data2)) == Arrays.equals(data1, data2) + * {@code createId(data1).equals(createId(data2)) == Arrays.equals(data1, data2)} * * @param serialized serialized data * @return raw node id as byte array diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/NotFoundException.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/NotFoundException.java similarity index 100% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/store/NotFoundException.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/store/NotFoundException.java diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/PersistHook.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/PersistHook.java similarity index 83% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/store/PersistHook.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/store/PersistHook.java index 66979cf3c8f..a89f53f4515 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/PersistHook.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/PersistHook.java @@ -21,6 +21,6 @@ */ public interface PersistHook { - void prePersist(RevisionStore store) throws Exception; - void postPersist(RevisionStore store) throws Exception; + void prePersist(RevisionStore store, RevisionStore.PutToken token) throws Exception; + void postPersist(RevisionStore store, RevisionStore.PutToken token) throws Exception; } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/RevisionProvider.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/RevisionProvider.java similarity index 87% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/store/RevisionProvider.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/store/RevisionProvider.java index 345a80f73d2..cfd50858bda 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/RevisionProvider.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/RevisionProvider.java @@ -20,12 +20,13 @@ import org.apache.jackrabbit.mk.model.Id; import org.apache.jackrabbit.mk.model.StoredCommit; import org.apache.jackrabbit.mk.model.StoredNode; -import org.apache.jackrabbit.oak.model.NodeState; +import org.apache.jackrabbit.mk.model.tree.NodeState; +import org.apache.jackrabbit.mk.model.tree.NodeStore; /** - * + * Read operations. */ -public interface RevisionProvider { +public interface RevisionProvider extends NodeStore { /** * Adapts the given {@link StoredNode} to a corresponding @@ -50,6 +51,4 @@ public interface RevisionProvider { StoredNode getRootNode(Id commitId) throws NotFoundException, Exception; StoredCommit getHeadCommit() throws Exception; Id getHeadCommitId() throws Exception; - int getBlob(String blobId, long pos, byte[] buff, int off, int length) throws NotFoundException, Exception; - long getBlobLength(String blobId) throws NotFoundException, Exception; } diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/RevisionStore.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/RevisionStore.java new file mode 100644 index 00000000000..07021f7dd2c --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/RevisionStore.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.store; + +import org.apache.jackrabbit.mk.model.ChildNodeEntries; +import org.apache.jackrabbit.mk.model.ChildNodeEntriesMap; +import org.apache.jackrabbit.mk.model.Id; +import org.apache.jackrabbit.mk.model.MutableCommit; +import org.apache.jackrabbit.mk.model.MutableNode; + +/** + * Write operations. + */ +public interface RevisionStore extends RevisionProvider { + + /** + * Token that must be created first before invoking any put operation. + */ + public abstract class PutToken { + + /* Prevent other implementations. */ + PutToken() {} + } + + /** + * Create a put token. + * + * @return put token + */ + PutToken createPutToken(); + + Id /*id*/ putNode(PutToken token, MutableNode node) throws Exception; + Id /*id*/ putCNEMap(PutToken token, ChildNodeEntries map) throws Exception; + + /** + * Lock the head. Must be called prior to putting a new head commit. + * + * @see #putHeadCommit(PutToken, MutableCommit, Id, Id) + * @see #unlockHead() + */ + void lockHead(); + + /** + * Put a new head commit. Must be called while holding a lock on the head. + * + * @param token + * put token + * @param commit + * commit + * @param branchRootId + * former branch root id, if this is a merge; otherwise + * {@code null} + * @return branchRevId + * current branch head, i.e. last commit on this branch, + * if this is a merge; otherwise {@code null} + * @return head commit id + * @throws Exception + * if an error occurs + * @see #lockHead() + */ + Id /*id*/ putHeadCommit(PutToken token, MutableCommit commit, Id branchRootId, Id branchRevId) throws Exception; + + /** + * Unlock the head. + * + * @see #lockHead() + */ + void unlockHead(); + + /** + * Store a new commit. + *

    + * Unlike {@code putHeadCommit(MutableCommit)}, this method + * does not affect the current head commit and therefore doesn't + * require a lock on the head. + * + * @param token put token + * @param commit commit + * @return new commit id + * @throws Exception if an error occurs + */ + Id /*id*/ putCommit(PutToken token, MutableCommit commit) throws Exception; +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/StoredNodeAsState.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/StoredNodeAsState.java similarity index 77% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/store/StoredNodeAsState.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/store/StoredNodeAsState.java index fe75b9916a0..2ddd5120022 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/store/StoredNodeAsState.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/StoredNodeAsState.java @@ -16,19 +16,20 @@ */ package org.apache.jackrabbit.mk.store; +import org.apache.jackrabbit.mk.model.ChildNodeEntry; +import org.apache.jackrabbit.mk.model.Id; +import org.apache.jackrabbit.mk.model.StoredNode; +import org.apache.jackrabbit.mk.model.tree.AbstractChildNode; +import org.apache.jackrabbit.mk.model.tree.AbstractNodeState; +import org.apache.jackrabbit.mk.model.tree.AbstractPropertyState; +import org.apache.jackrabbit.mk.model.tree.ChildNode; +import org.apache.jackrabbit.mk.model.tree.NodeState; +import org.apache.jackrabbit.mk.model.tree.PropertyState; + import java.util.Collections; import java.util.Iterator; import java.util.Map; -import org.apache.jackrabbit.mk.model.Id; -import org.apache.jackrabbit.mk.model.StoredNode; -import org.apache.jackrabbit.oak.model.AbstractChildNodeEntry; -import org.apache.jackrabbit.oak.model.AbstractNodeState; -import org.apache.jackrabbit.oak.model.AbstractPropertyState; -import org.apache.jackrabbit.oak.model.ChildNodeEntry; -import org.apache.jackrabbit.oak.model.NodeState; -import org.apache.jackrabbit.oak.model.PropertyState; - class StoredNodeAsState extends AbstractNodeState { private final StoredNode node; @@ -44,12 +45,15 @@ Id getId() { return node.getId(); } - private static class SimplePropertyState extends AbstractPropertyState { + StoredNode unwrap() { + return node; + } + private static class SimplePropertyState extends AbstractPropertyState { private final String name; - private final String value; + // todo make name and value not nullable public SimplePropertyState(String name, String value) { this.name = name; this.value = value; @@ -85,18 +89,22 @@ public long getPropertyCount() { @Override public Iterable getProperties() { return new Iterable() { + @Override public Iterator iterator() { final Iterator> iterator = node.getProperties().entrySet().iterator(); return new Iterator() { + @Override public boolean hasNext() { return iterator.hasNext(); } + @Override public PropertyState next() { Map.Entry entry = iterator.next(); return new SimplePropertyState( entry.getKey(), entry.getValue()); } + @Override public void remove() { throw new UnsupportedOperationException(); } @@ -107,8 +115,7 @@ public void remove() { @Override public NodeState getChildNode(String name) { - org.apache.jackrabbit.mk.model.ChildNodeEntry entry = - node.getChildNodeEntry(name); + ChildNodeEntry entry = node.getChildNodeEntry(name); if (entry != null) { return getChildNodeEntry(entry).getNode(); } else { @@ -122,28 +129,28 @@ public long getChildNodeCount() { } @Override - public Iterable getChildNodeEntries( - final long offset, final long length) { - if (length < -1) { - throw new IllegalArgumentException("Illegal length: " + length); + public Iterable getChildNodeEntries( + final long offset, final int count) { + if (count < -1) { + throw new IllegalArgumentException("Illegal count: " + count); } else if (offset > Integer.MAX_VALUE) { return Collections.emptyList(); } else { - return new Iterable() { - public Iterator iterator() { - int count = -1; - if (length < Integer.MAX_VALUE) { - count = (int) length; - } - final Iterator iterator = + return new Iterable() { + @Override + public Iterator iterator() { + final Iterator iterator = node.getChildNodeEntries((int) offset, count); - return new Iterator() { + return new Iterator() { + @Override public boolean hasNext() { return iterator.hasNext(); } - public ChildNodeEntry next() { + @Override + public ChildNode next() { return getChildNodeEntry(iterator.next()); } + @Override public void remove() { throw new UnsupportedOperationException(); } @@ -153,12 +160,14 @@ public void remove() { } } - private ChildNodeEntry getChildNodeEntry( - final org.apache.jackrabbit.mk.model.ChildNodeEntry entry) { - return new AbstractChildNodeEntry() { + private ChildNode getChildNodeEntry( + final ChildNodeEntry entry) { + return new AbstractChildNode() { + @Override public String getName() { return entry.getName(); } + @Override public NodeState getNode() { try { StoredNode child = provider.getNode(entry.getId()); diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/AbstractFilteringIterator.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/AbstractFilteringIterator.java similarity index 100% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/util/AbstractFilteringIterator.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/util/AbstractFilteringIterator.java diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/AbstractRangeIterator.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/AbstractRangeIterator.java similarity index 100% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/util/AbstractRangeIterator.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/util/AbstractRangeIterator.java diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/Cache.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/Cache.java similarity index 100% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/util/Cache.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/util/Cache.java diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/CommitGate.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/CommitGate.java similarity index 100% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/util/CommitGate.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/util/CommitGate.java diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/IOUtils.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/IOUtils.java similarity index 93% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/util/IOUtils.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/util/IOUtils.java index 97ae2689ab2..8258a4c900f 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/IOUtils.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/IOUtils.java @@ -108,7 +108,7 @@ public static void writeBytes(OutputStream out, byte[] data) throws IOException } /** - * Read a byte array. This will first read the length as 4 bytes, and then + * Read a byte array. This will first read the length as 4 bytes, and then * the actual bytes. * * @param in the data input stream @@ -154,7 +154,11 @@ public static int readVarInt(InputStream in) throws IOException { } x &= 0x7f; for (int s = 7;; s += 7) { - int b = (byte) in.read(); + int b = in.read(); + if (b < 0) { + throw new EOFException(); + } + b = (byte) b; x |= (b & 0x7f) << s; if (b >= 0) { return x; @@ -237,7 +241,11 @@ public static long readVarLong(InputStream in) throws IOException { } x &= 0x7f; for (int s = 7;; s += 7) { - long b = (byte) in.read(); + long b = in.read(); + if (b < 0) { + throw new EOFException(); + } + b = (byte) b; x |= (b & 0x7f) << s; if (b >= 0) { return x; @@ -261,7 +269,7 @@ public static int nextPowerOf2(int x) { } /** - * Unconditionally close a Closeable. + * Unconditionally close a {@code Closeable}. *

    * Equivalent to {@link Closeable#close()}, except any exceptions will be ignored. * This is typically used in finally blocks. @@ -279,7 +287,7 @@ public static void closeQuietly(Closeable closeable) { } /** - * Unconditionally close a Socket. + * Unconditionally close a {@code Socket}. *

    * Equivalent to {@link Socket#close()}, except any exceptions will be ignored. * This is typically used in finally blocks. @@ -297,14 +305,14 @@ public static void closeQuietly(Socket sock) { } /** - * Copy bytes from an InputStream to an - * OutputStream. + * Copy bytes from an {@code InputStream} to an + * {@code OutputStream}. *

    * This method buffers the input internally, so there is no need to use a - * BufferedInputStream. + * {@code BufferedInputStream}. * - * @param input the InputStream to read from - * @param output the OutputStream to write to + * @param input the {@code InputStream} to read from + * @param output the {@code OutputStream} to write to * @return the number of bytes copied * @throws IOException if an I/O error occurs */ diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/MicroKernelInputStream.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/MicroKernelInputStream.java similarity index 83% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/util/MicroKernelInputStream.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/util/MicroKernelInputStream.java index 9b1ce238fd9..b028ab04111 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/MicroKernelInputStream.java +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/MicroKernelInputStream.java @@ -22,14 +22,14 @@ import java.io.InputStream; /** - * An input stream to simplify reading a blob from the micro kernel. - * See also BlobStoreInputStream. + * An input stream to simplify reading a blob from a {@code MicroKernel}. */ public class MicroKernelInputStream extends InputStream { private final MicroKernel mk; private final String id; private long pos; + private long length = -1; private byte[] oneByteBuff; public MicroKernelInputStream(MicroKernel mk, String id) { @@ -37,6 +37,20 @@ public MicroKernelInputStream(MicroKernel mk, String id) { this.id = id; } + @Override + public long skip(long n) { + if (n < 0) { + return 0; + } + if (length == -1) { + length = mk.getLength(id); + } + n = Math.min(n, length - pos); + pos += n; + return n; + } + + @Override public int read(byte[] b, int off, int len) { int l = mk.read(id, pos, b, off, len); if (l < 0) { @@ -46,6 +60,7 @@ public int read(byte[] b, int off, int len) { return l; } + @Override public int read() throws IOException { if (oneByteBuff == null) { oneByteBuff = new byte[1]; diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/NameFilter.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/NameFilter.java new file mode 100644 index 00000000000..38d115e4cee --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/NameFilter.java @@ -0,0 +1,192 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.util; + +import java.util.ArrayList; +import java.util.List; + +/** + * Simple name filter utility class. + *

      + *
    • a filter consists of one or more globs
    • + *
    • a glob prefixed by {@code -} (dash) is treated as an exclusion pattern; + * all others are considered inclusion patterns
    • + *
    • a leading {@code -} (dash) must be escaped by prepending {@code \} (backslash) + * if it should be interpreted as a literal
    • + *
    • {@code *} (asterisk) serves as a wildcard, i.e. it matches any + * substring in the target name
    • + *
    • {@code *} (asterisk) occurrences within the glob to be interpreted as + * literals must be escaped by prepending {@code \} (backslash)
    • + *
    • a filter matches a target name if any of the inclusion patterns match but + * none of the exclusion patterns
    • + *
    + * Examples: + *

    + * {@code ["foo*", "-foo99"]} matches {@code "foo"} and {@code "foo bar"} + * but not {@code "foo99"}. + *

    + * {@code ["foo\*"]} matches {@code "foo*"} but not {@code "foo99"}. + *

    + * {@code ["\-blah"]} matches {@code "-blah"}. + */ +public class NameFilter { + + public static final char WILDCARD = '*'; + public static final char EXCLUDE_PREFIX = '-'; + public static final char ESCAPE = '\\'; + + // list of ORed inclusion patterns + private final List inclPatterns = new ArrayList(); + + // list of ORed exclusion patterns + private final List exclPatterns = new ArrayList(); + + private boolean containsWildcard; + + public NameFilter(String[] patterns) { + containsWildcard = false; + for (String pattern : patterns) { + if (pattern.isEmpty()) { + continue; + } else if (pattern.charAt(0) == EXCLUDE_PREFIX) { + pattern = pattern.substring(1); + exclPatterns.add(pattern); + } else { + inclPatterns.add(pattern); + } + if (!containsWildcard) { + containsWildcard = containsWildCard(pattern); + } + } + } + + public boolean matches(String name) { + boolean matched = false; + // check inclusion patterns + for (String pattern : inclPatterns) { + if (internalMatches(name, pattern, 0, 0)) { + matched = true; + break; + } + } + if (matched) { + // check exclusion patterns + for (String pattern : exclPatterns) { + if (internalMatches(name, pattern, 0, 0)) { + matched = false; + break; + } + } + } + return matched; + } + + public boolean containsWildcard() { + return containsWildcard; + } + + public List getExclusionPatterns() { + return exclPatterns; + } + + public List getInclusionPatterns() { + return inclPatterns; + } + + private static boolean containsWildCard(String pattern) { + int len = pattern.length(); + int pos = 0; + while (pos < len) { + if (pattern.charAt(pos) == ESCAPE + && pos < (len - 1) + && pattern.charAt(pos + 1) == WILDCARD) { + pos += 2; + continue; + } + if (pattern.charAt(pos) == WILDCARD) { + return true; + } + pos++; + } + return false; + } + + /** + * Internal helper used to recursively match the pattern + * + * @param s The string to be tested + * @param pattern The pattern + * @param sOff offset within s + * @param pOff offset within pattern. + * @return true if s matched pattern, else false. + */ + private static boolean internalMatches(String s, String pattern, + int sOff, int pOff) { + int pLen = pattern.length(); + int sLen = s.length(); + + while (true) { + if (pOff >= pLen) { + return sOff >= sLen ? true : false; + } + if (sOff >= sLen && pattern.charAt(pOff) != WILDCARD) { + return false; + } + + // check for a WILDCARD as the next pattern; + // this is handled by a recursive call for + // each postfix of the name. + if (pattern.charAt(pOff) == WILDCARD) { + ++pOff; + if (pOff >= pLen) { + return true; + } + + while (true) { + if (internalMatches(s, pattern, sOff, pOff)) { + return true; + } + if (sOff >= sLen) { + return false; + } + sOff++; + } + } + + if (pOff < pLen && sOff < sLen) { + // check for escape sequences + if (pattern.charAt(pOff) == ESCAPE) { + // * to be interpreted as literal + if (pOff < pLen - 1 + && pattern.charAt(pOff + 1) == WILDCARD) { + ++pOff; + } + // leading - to be interpreted as literal + if (pOff == 0 && pLen > 1 + && pattern.charAt(pOff + 1) == EXCLUDE_PREFIX) { + ++pOff; + } + } + if (pattern.charAt(pOff) != s.charAt(sOff)) { + return false; + } + } + pOff++; + sOff++; + } + } +} diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/NodeFilter.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/NodeFilter.java new file mode 100644 index 00000000000..756e9289f10 --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/NodeFilter.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.util; + +import org.apache.jackrabbit.mk.json.JsopTokenizer; + +import java.util.ArrayList; +import java.util.List; + +/** + * A {@code NodeFilter} represents a filter on property and/or node names specified + * in JSON format. It allows to specify glob patterns for names of nodes and/or + * properties to be included or excluded. + *

    + * Example: + *

    + * {
    + *   "nodes": [ "foo*", "-foo1" ],
    + *   "properties": [ "*", "-:childNodeCount" ]
    + * }
    + * 
    + * + * @see NameFilter + * @see org.apache.jackrabbit.mk.api.MicroKernel#getNodes(String, String, int, long, int, String) + */ +public class NodeFilter { + + private final NameFilter nodeFilter; + private final NameFilter propFilter; + + private NodeFilter(NameFilter nodeFilter, NameFilter propFilter) { + this.nodeFilter = nodeFilter; + this.propFilter = propFilter; + } + + public static NodeFilter parse(String json) { + // parse json format filter + JsopTokenizer t = new JsopTokenizer(json); + t.read('{'); + + NameFilter nodeFilter = null, propFilter = null; + + do { + String type = t.readString(); + t.read(':'); + String[] globs = parseArray(t); + if (type.equals("nodes")) { + nodeFilter = new NameFilter(globs); + } else if (type.equals("properties")) { + propFilter = new NameFilter(globs); + } else { + throw new IllegalArgumentException("illegal filter format"); + } + } while (t.matches(',')); + t.read('}'); + + return new NodeFilter(nodeFilter, propFilter); + } + + private static String[] parseArray(JsopTokenizer t) { + List l = new ArrayList(); + t.read('['); + do { + l.add(t.readString()); + } while (t.matches(',')); + t.read(']'); + return l.toArray(new String[l.size()]); + } + + public NameFilter getChildNodeFilter() { + return nodeFilter; + } + + public NameFilter getPropertyFilter() { + return propFilter; + } + + public boolean includeNode(String name) { + return nodeFilter == null || nodeFilter.matches(name); + } + + public boolean includeProperty(String name) { + return propFilter == null || propFilter.matches(name); + } +} \ No newline at end of file diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/RangeIterator.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/RangeIterator.java similarity index 100% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/util/RangeIterator.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/util/RangeIterator.java diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/SimpleLRUCache.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/SimpleLRUCache.java similarity index 100% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/util/SimpleLRUCache.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/util/SimpleLRUCache.java diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/StringUtils.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/StringUtils.java similarity index 100% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/util/StringUtils.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/util/StringUtils.java diff --git a/oak-core/src/main/java/org/apache/jackrabbit/mk/util/UnmodifiableIterator.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/UnmodifiableIterator.java similarity index 100% rename from oak-core/src/main/java/org/apache/jackrabbit/mk/util/UnmodifiableIterator.java rename to oak-mk/src/main/java/org/apache/jackrabbit/mk/util/UnmodifiableIterator.java diff --git a/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/package-info.java b/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/package-info.java new file mode 100644 index 00000000000..a41a63d68e3 --- /dev/null +++ b/oak-mk/src/main/java/org/apache/jackrabbit/mk/util/package-info.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +@Version("0.1") +@Export(optional = "provide:=true") +package org.apache.jackrabbit.mk.util; + +import aQute.bnd.annotation.Export; +import aQute.bnd.annotation.Version; + diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/ConcurrentWriteTest.java b/oak-mk/src/test/java/org/apache/jackrabbit/mk/ConcurrentWriteIT.java similarity index 63% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/ConcurrentWriteTest.java rename to oak-mk/src/test/java/org/apache/jackrabbit/mk/ConcurrentWriteIT.java index 01e23cbd6fa..09abe16d6d1 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/ConcurrentWriteTest.java +++ b/oak-mk/src/test/java/org/apache/jackrabbit/mk/ConcurrentWriteIT.java @@ -16,33 +16,26 @@ */ package org.apache.jackrabbit.mk; -import java.util.Random; import junit.framework.TestCase; import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; -public class ConcurrentWriteTest extends TestCase { +import java.util.Random; - protected static final String TEST_PATH = "/" + ConcurrentWriteTest.class.getName(); +public class ConcurrentWriteIT extends TestCase { - private static final String URL = "fs:{homeDir}/target;clean"; - // private static final String URL = "fs:{homeDir}/target"; - // private static final String URL = "simple:"; - //private static final String URL = "simple:fs:target/temp;clean"; + protected static final String TEST_PATH = "/" + ConcurrentWriteIT.class.getName(); private static final int NUM_THREADS = 20; private static final int NUM_CHILDNODES = 1000; - MicroKernel mk; + final MicroKernel mk = new MicroKernelImpl(); public void setUp() throws Exception { - mk = MicroKernelFactory.getInstance(URL); - mk.commit("/", "+ \"" + TEST_PATH.substring(1) + "\": {\"jcr:primaryType\":\"nt:unstructured\"}", mk.getHeadRevision(), null); + mk.commit("/", "+ \"" + TEST_PATH.substring(1) + "\": {\"jcr:primaryType\":\"nt:unstructured\"}", null, null); } public void tearDown() throws InterruptedException { - String head = mk.commit("/", "- \"" + TEST_PATH.substring(1) + "\"", mk.getHeadRevision(), null); - System.out.println("new HEAD: " + head); - mk.dispose(); } /** @@ -50,49 +43,31 @@ public void tearDown() throws InterruptedException { */ public void testConcurrentWriting() throws Exception { - Profiler prof = new Profiler(); - prof.depth = 8; - prof.interval = 1; - // prof.startCollecting(); - String oldHead = mk.getHeadRevision(); - long t0 = System.currentTimeMillis(); - TestThread[] threads = new TestThread[NUM_THREADS]; for (int i = 0; i < threads.length; i++) { TestThread thread = new TestThread(oldHead, "t" + i); - thread.start(); threads[i] = thread; + + assertFalse(mk.nodeExists(TEST_PATH + "/" + thread.getName(), null)); } + // long t0 = System.currentTimeMillis(); + for (TestThread t : threads) { if (t != null) { + t.start(); t.join(); } } - long t1 = System.currentTimeMillis(); + // long t1 = System.currentTimeMillis(); + // System.out.println("duration: " + (t1 - t0) + "ms"); - System.out.println("duration: " + (t1 - t0) + "ms"); - - String head = mk.getHeadRevision(); - mk.getNodes("/", head, Integer.MAX_VALUE, 0, -1, null); - // System.out.println(json); - System.out.println("new HEAD: " + head); - System.out.println(); - - String history = mk.getRevisions(t0, -1); - System.out.println("History:"); - System.out.println(history); - System.out.println(); - - mk.getJournal(oldHead, head, null); - // System.out.println("Journal:"); - // System.out.println(journal); - // System.out.println(); - - // System.out.println(prof.getTop(5)); + for (Thread t : threads) { + assertTrue(mk.nodeExists(TEST_PATH + "/" + t.getName(), null)); + } } class TestThread extends Thread { diff --git a/oak-mk/src/test/java/org/apache/jackrabbit/mk/MicroKernelImplTest.java b/oak-mk/src/test/java/org/apache/jackrabbit/mk/MicroKernelImplTest.java new file mode 100644 index 00000000000..24424680bfd --- /dev/null +++ b/oak-mk/src/test/java/org/apache/jackrabbit/mk/MicroKernelImplTest.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk; + +import java.io.File; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class MicroKernelImplTest { + + private File homeDir; + private MicroKernelImpl mk; + + @Before + public void setup() throws Exception { + homeDir = new File("target/mk"); + if (homeDir.exists()) { + FileUtils.cleanDirectory(homeDir); + } + mk = new MicroKernelImpl(homeDir.getPath()); + } + + @After + public void tearDown() throws Exception { + if (mk != null) { + mk.dispose(); + } + } + + /** + * OAK-276: potential clash of commit id's after restart. + */ + @Test + public void potentialClashOfCommitIds() { + String headRev = mk.commit("/", "+\"a\" : {}", mk.getHeadRevision(), null); + String branchRev = mk.branch(mk.getHeadRevision()); + + mk.dispose(); + mk = new MicroKernelImpl(homeDir.getPath()); + assertEquals("Stored head should be equal", headRev, mk.getHeadRevision()); + + headRev = mk.commit("/", "+\"b\" : {}", mk.getHeadRevision(), null); + assertFalse("Commit must not have same id as branch", headRev.equals(branchRev)); + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/blobs/DbBlobStoreTest.java b/oak-mk/src/test/java/org/apache/jackrabbit/mk/blobs/AbstractBlobStoreTest.java similarity index 88% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/blobs/DbBlobStoreTest.java rename to oak-mk/src/test/java/org/apache/jackrabbit/mk/blobs/AbstractBlobStoreTest.java index 4abeba668d0..50a2e9ed66a 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/blobs/DbBlobStoreTest.java +++ b/oak-mk/src/test/java/org/apache/jackrabbit/mk/blobs/AbstractBlobStoreTest.java @@ -17,12 +17,9 @@ package org.apache.jackrabbit.mk.blobs; import junit.framework.TestCase; -import org.apache.jackrabbit.mk.api.MicroKernelException; -import org.apache.jackrabbit.mk.fs.FileUtils; import org.apache.jackrabbit.mk.json.JsopBuilder; import org.apache.jackrabbit.mk.json.JsopTokenizer; import org.apache.jackrabbit.mk.util.IOUtilsTest; -import org.h2.jdbcx.JdbcConnectionPool; import java.io.ByteArrayInputStream; import java.io.File; @@ -31,7 +28,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.sql.Connection; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -39,41 +35,33 @@ import java.util.concurrent.atomic.AtomicBoolean; /** - * Tests the DbBlobStore implementation. + * Tests a BlobStore implementation. */ -public class DbBlobStoreTest extends TestCase { +public abstract class AbstractBlobStoreTest extends TestCase { protected AbstractBlobStore store; - private Connection sentinel; - - public void setUp() throws Exception { - Class.forName("org.h2.Driver"); - JdbcConnectionPool cp = JdbcConnectionPool.create("jdbc:h2:mem:", "", ""); - sentinel = cp.getConnection(); - DbBlobStore blobStore = new DbBlobStore(); - blobStore.setConnectionPool(cp); - blobStore.setBlockSize(128); - blobStore.setBlockSizeMin(32); - this.store = blobStore; - } + + /** + * Should be overridden by subclasses to set the {@link #store} variable. + */ + public abstract void setUp() throws Exception; public void tearDown() throws Exception { - if (sentinel != null) { - sentinel.close(); - } - store.close(); + store = null; } - public void testAddFile() throws Exception { + public void testWriteFile() throws Exception { store.setBlockSize(1024 * 1024); byte[] data = new byte[4 * 1024 * 1024]; Random r = new Random(0); r.nextBytes(data); String tempFileName = "target/temp/test"; - OutputStream out = FileUtils.newOutputStream(tempFileName, false); + File tempFile = new File(tempFileName); + tempFile.getParentFile().mkdirs(); + OutputStream out = new FileOutputStream(tempFile, false); out.write(data); out.close(); - String s = store.addBlob(tempFileName); + String s = store.writeBlob(tempFileName); assertEquals(data.length, store.getBlobLength(s)); byte[] buff = new byte[1]; for (int i = 0; i < data.length; i += 1024) { @@ -81,7 +69,7 @@ public void testAddFile() throws Exception { assertEquals(data[i], buff[0]); } try { - store.addBlob(tempFileName + "_wrong"); + store.writeBlob(tempFileName + "_wrong"); fail(); } catch (Exception e) { // expected @@ -141,13 +129,13 @@ public void testIllegalIdentifier() throws Exception { try { store.readBlob("ff", 0, data, 0, 1); fail(); - } catch (MicroKernelException e) { + } catch (Exception e) { // expected } try { store.getBlobLength("ff"); fail(); - } catch (MicroKernelException e) { + } catch (Exception e) { // expected } try { @@ -174,7 +162,7 @@ public void testGarbageCollection() throws Exception { HashMap map = new HashMap(); ArrayList mem = new ArrayList(); int count; - for (int i = 1; i < 10000; i += (i + 1) * 10) { + for (int i = 1; i <= 1000; i *= 10) { byte[] data = new byte[i]; String id; id = store.writeBlob(new ByteArrayInputStream(data)); @@ -206,10 +194,19 @@ public void testGarbageCollection() throws Exception { } store.mark(id); } - store.sweep(); + count = store.sweep(); store.clearInUse(); store.clearCache(); + + // https://issues.apache.org/jira/browse/OAK-60 + // endure there is at least one old entry (with age 1 ms) + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // ignore + } + store.startMark(); count = store.sweep(); assertTrue("count: " + count, count > 0); @@ -224,13 +221,6 @@ public void testGarbageCollection() throws Exception { } } assertTrue("failedCount: " + failedCount, failedCount > 0); - - -// try { -// Thread.sleep(10); -// } catch (InterruptedException e) { -// -// } } private void doTest(int maxLength, int count) throws Exception { @@ -284,7 +274,6 @@ public static void main(String... args) throws Exception { String id = addFiles(store, "~/temp/ds"); extractFiles(store, id, "target/test"); - store.close(); } public static void extractFiles(AbstractBlobStore store, String listingId, String target) throws IOException { diff --git a/oak-mk/src/test/java/org/apache/jackrabbit/mk/blobs/DbBlobStoreTest.java b/oak-mk/src/test/java/org/apache/jackrabbit/mk/blobs/DbBlobStoreTest.java new file mode 100644 index 00000000000..cbb81078376 --- /dev/null +++ b/oak-mk/src/test/java/org/apache/jackrabbit/mk/blobs/DbBlobStoreTest.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.blobs; + +import org.h2.jdbcx.JdbcConnectionPool; + +import java.sql.Connection; + +/** + * Tests the DbBlobStore implementation. + */ +public class DbBlobStoreTest extends AbstractBlobStoreTest { + + private Connection sentinel; + private JdbcConnectionPool cp; + + public void setUp() throws Exception { + Class.forName("org.h2.Driver"); + cp = JdbcConnectionPool.create("jdbc:h2:mem:", "", ""); + sentinel = cp.getConnection(); + DbBlobStore blobStore = new DbBlobStore(); + blobStore.setConnectionPool(cp); + blobStore.setBlockSize(128); + blobStore.setBlockSizeMin(48); + this.store = blobStore; + } + + public void tearDown() throws Exception { + super.tearDown(); + if (sentinel != null) { + sentinel.close(); + } + cp.dispose(); + } + +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/blobs/FileBlobStoreTest.java b/oak-mk/src/test/java/org/apache/jackrabbit/mk/blobs/FileBlobStoreTest.java similarity index 91% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/blobs/FileBlobStoreTest.java rename to oak-mk/src/test/java/org/apache/jackrabbit/mk/blobs/FileBlobStoreTest.java index cf1c433acdb..41bb0c68e45 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/blobs/FileBlobStoreTest.java +++ b/oak-mk/src/test/java/org/apache/jackrabbit/mk/blobs/FileBlobStoreTest.java @@ -19,12 +19,12 @@ /** * Tests the FileBlobStore implementation. */ -public class FileBlobStoreTest extends DbBlobStoreTest { +public class FileBlobStoreTest extends AbstractBlobStoreTest { public void setUp() throws Exception { FileBlobStore store = new FileBlobStore("target/temp"); store.setBlockSize(128); - store.setBlockSizeMin(32); + store.setBlockSizeMin(48); this.store = store; } diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/blobs/MemoryBlobStoreTest.java b/oak-mk/src/test/java/org/apache/jackrabbit/mk/blobs/MemoryBlobStoreTest.java similarity index 87% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/blobs/MemoryBlobStoreTest.java rename to oak-mk/src/test/java/org/apache/jackrabbit/mk/blobs/MemoryBlobStoreTest.java index 50ec45f8fb4..9852eb75b16 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/blobs/MemoryBlobStoreTest.java +++ b/oak-mk/src/test/java/org/apache/jackrabbit/mk/blobs/MemoryBlobStoreTest.java @@ -19,10 +19,12 @@ /** * Tests the MemoryBlobStore implementation. */ -public class MemoryBlobStoreTest extends DbBlobStoreTest { +public class MemoryBlobStoreTest extends AbstractBlobStoreTest { public void setUp() throws Exception { store = new MemoryBlobStore(); - } + store.setBlockSize(128); + store.setBlockSizeMin(48); + } } diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/Concurrent.java b/oak-mk/src/test/java/org/apache/jackrabbit/mk/concurrent/Concurrent.java similarity index 86% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/util/Concurrent.java rename to oak-mk/src/test/java/org/apache/jackrabbit/mk/concurrent/Concurrent.java index 6ea04b98789..a9d7d9bee67 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/Concurrent.java +++ b/oak-mk/src/test/java/org/apache/jackrabbit/mk/concurrent/Concurrent.java @@ -14,22 +14,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.mk.util; +package org.apache.jackrabbit.mk.concurrent; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import org.h2.util.Profiler; /** * A concurrency test tool. */ public class Concurrent { - private static final boolean BENCHMARK = false; private static final boolean PROFILE = false; + private Concurrent() { + } + /** * Run a task concurrently in 2 threads for 1 second. * @@ -46,14 +47,9 @@ public static void run(String message, final Task task, int threadCount, int mil final AtomicReference exception = new AtomicReference(); ArrayList threads = new ArrayList(); final AtomicInteger counter = new AtomicInteger(); - StopWatch timer = new StopWatch(); - Profiler p = new Profiler(); - if (PROFILE) { - p.interval = 1; - p.startCollecting(); - } for (int i = 0; i < threadCount; i++) { Thread t = new Thread("Task " + i) { + @Override public void run() { while (!stopped.get()) { try { @@ -97,13 +93,6 @@ public void run() { } throw (Error) e; } - if (BENCHMARK) { - String t = timer.operationsPerSecond(counter.get()); - System.out.println(message + ": " + t); - } - if (PROFILE) { - System.out.println(p.getTop(5)); - } } public interface Task { diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/concurrent/ConcurrentBlobTest.java b/oak-mk/src/test/java/org/apache/jackrabbit/mk/concurrent/ConcurrentBlobTest.java similarity index 89% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/concurrent/ConcurrentBlobTest.java rename to oak-mk/src/test/java/org/apache/jackrabbit/mk/concurrent/ConcurrentBlobTest.java index 0a7413bbcd2..1ec482955e8 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/concurrent/ConcurrentBlobTest.java +++ b/oak-mk/src/test/java/org/apache/jackrabbit/mk/concurrent/ConcurrentBlobTest.java @@ -16,18 +16,17 @@ */ package org.apache.jackrabbit.mk.concurrent; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.util.concurrent.atomic.AtomicInteger; import junit.framework.Assert; import org.apache.jackrabbit.mk.blobs.MemoryBlobStore; -import org.apache.jackrabbit.mk.util.Concurrent; import org.apache.jackrabbit.mk.util.IOUtils; -import org.apache.jackrabbit.mk.util.IOUtilsTest; -import org.apache.jackrabbit.mk.util.Concurrent.Task; import org.junit.Before; import org.junit.Test; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + /** * Test concurrent access to the blob store. */ @@ -45,7 +44,8 @@ public void setUp() throws Exception { @Test public void test() throws Exception { final AtomicInteger id = new AtomicInteger(); - Concurrent.run("blob", new Task() { + Concurrent.run("blob", new Concurrent.Task() { + @Override public void call() throws Exception { int i = id.getAndIncrement(); ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -56,7 +56,7 @@ public void call() throws Exception { Assert.assertEquals(58, store.getBlobLength(id)); byte[] test = out.toByteArray(); Assert.assertEquals(8, store.readBlob(id, 0, test, 0, 8)); - IOUtilsTest.assertEquals(data, test); + Assert.assertTrue(Arrays.equals(data, test)); } }); } diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/concurrent/ConcurrentCacheTest.java b/oak-mk/src/test/java/org/apache/jackrabbit/mk/concurrent/ConcurrentCacheTest.java similarity index 93% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/concurrent/ConcurrentCacheTest.java rename to oak-mk/src/test/java/org/apache/jackrabbit/mk/concurrent/ConcurrentCacheTest.java index ba0fea023e1..200e9e89b4f 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/concurrent/ConcurrentCacheTest.java +++ b/oak-mk/src/test/java/org/apache/jackrabbit/mk/concurrent/ConcurrentCacheTest.java @@ -16,13 +16,12 @@ */ package org.apache.jackrabbit.mk.concurrent; -import java.util.concurrent.atomic.AtomicInteger; import junit.framework.Assert; import org.apache.jackrabbit.mk.util.Cache; -import org.apache.jackrabbit.mk.util.Concurrent; -import org.apache.jackrabbit.mk.util.Concurrent.Task; import org.junit.Test; +import java.util.concurrent.atomic.AtomicInteger; + /** * Tests the cache implementation. */ @@ -34,7 +33,8 @@ public class ConcurrentCacheTest implements Cache.Backend pmClass; + private GCPersistence pm; + + public GCPersistenceTest(Class pmClass) { + this.pmClass = pmClass; + } + + @Before + public void setup() throws Exception { + pm = pmClass.newInstance(); + pm.initialize(new File("target/mk")); + + // start with empty repository + pm.start(); + pm.sweep(); + } + + @SuppressWarnings("rawtypes") + @Parameters + public static Collection classes() { + Class[][] pmClasses = new Class[][] { + { H2Persistence.class }, + { InMemPersistence.class } + }; + return Arrays.asList(pmClasses); + } + + @After + public void tearDown() throws Exception { + IOUtils.closeQuietly(pm); + } + + @Test + public void testOldNodeIsSwept() throws Exception { + MutableNode node = new MutableNode(null); + Id id = pm.writeNode(node); + + Thread.sleep(1); + pm.start(); + pm.sweep(); + + try { + pm.readNode(new StoredNode(id, null)); + fail(); + } catch (NotFoundException e) { + /* expected */ + } + } + + @Test + public void testMarkedNodeIsNotSwept() throws Exception { + MutableNode node = new MutableNode(null); + Id id = pm.writeNode(node); + + // small delay needed + Thread.sleep(100); + + pm.start(); + + // old node must not be marked + assertTrue(pm.markNode(id)); + + pm.sweep(); + pm.readNode(new StoredNode(id, null)); + } + + @Test + public void testNewNodeIsNotSwept() throws Exception { + pm.start(); + + MutableNode node = new MutableNode(null); + Id id = pm.writeNode(node); + + // new node must already be marked + assertFalse(pm.markNode(id)); + + pm.sweep(); + pm.readNode(new StoredNode(id, null)); + } + + @Test + public void testReplaceCommit() throws Exception { + MutableCommit c1 = new MutableCommit(); + c1.setRootNodeId(Id.fromLong(0)); + pm.writeCommit(Id.fromLong(1), c1); + + MutableCommit c2 = new MutableCommit(); + c2.setParentId(c1.getId()); + c2.setRootNodeId(Id.fromLong(0)); + pm.writeCommit(Id.fromLong(2), c2); + + pm.start(); + c2 = new MutableCommit(); + c2.setRootNodeId(Id.fromLong(0)); + pm.replaceCommit(Id.fromLong(2), c2); + pm.sweep(); + + assertEquals(null, pm.readCommit(Id.fromLong(2)).getParentId()); + } +} + diff --git a/oak-mk/src/test/java/org/apache/jackrabbit/mk/store/DefaultRevisionStoreTest.java b/oak-mk/src/test/java/org/apache/jackrabbit/mk/store/DefaultRevisionStoreTest.java new file mode 100644 index 00000000000..746bc6d958e --- /dev/null +++ b/oak-mk/src/test/java/org/apache/jackrabbit/mk/store/DefaultRevisionStoreTest.java @@ -0,0 +1,210 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.store; + +import org.apache.jackrabbit.mk.blobs.MemoryBlobStore; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +import org.apache.jackrabbit.mk.core.Repository; +import org.apache.jackrabbit.mk.model.Id; +import org.apache.jackrabbit.mk.model.StoredCommit; +import org.apache.jackrabbit.mk.persistence.GCPersistence; +import org.apache.jackrabbit.mk.persistence.InMemPersistence; +import org.json.simple.JSONArray; +import org.json.simple.parser.JSONParser; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Tests verifying the inner workings of DefaultRevisionStore. + */ +public class DefaultRevisionStoreTest { + + /* avoid synthetic accessor */ DefaultRevisionStore rs; + private MicroKernelImpl mk; + + @Before + public void setup() throws Exception { + rs = new DefaultRevisionStore(createPersistence()) { + @Override + protected Id markCommits() throws Exception { + // Keep head commit only + StoredCommit commit = getHeadCommit(); + markCommit(commit); + return commit.getId(); + } + }; + rs.initialize(); + + mk = new MicroKernelImpl(new Repository(rs, new MemoryBlobStore())); + } + + protected GCPersistence createPersistence() throws Exception { + return new InMemPersistence(); + } + + @After + public void tearDown() throws Exception { + if (mk != null) { + mk.dispose(); + } + } + + /** + * Verify revision history works with garbage collection. + * + * @throws Exception if an error occurs + */ + @Test + public void testRevisionHistory() { + mk.commit("/", "+\"a\" : { \"c\":{}, \"d\":{} }", mk.getHeadRevision(), null); + mk.commit("/", "+\"b\" : {}", mk.getHeadRevision(), null); + mk.commit("/b", "+\"e\" : {}", mk.getHeadRevision(), null); + mk.commit("/a/c", "+\"f\" : {}", mk.getHeadRevision(), null); + + String headRevision = mk.getHeadRevision(); + String contents = mk.getNodes("/", headRevision, 1, 0, -1, null); + + rs.gc(); + + assertEquals(headRevision, mk.getHeadRevision()); + assertEquals(contents, mk.getNodes("/", headRevision, 1, 0, -1, null)); + + String history = mk.getRevisionHistory(Long.MIN_VALUE, Integer.MIN_VALUE, null); + assertEquals(1, parseJSONArray(history).size()); + } + + /** + * Verify branch and merge works with garbage collection. + * + * @throws Exception if an error occurs + */ + @Test + public void testBranchMerge() throws Exception { + mk.commit("/", "+\"a\" : { \"b\":{}, \"c\":{} }", mk.getHeadRevision(), null); + String branchRevisionId = mk.branch(mk.getHeadRevision()); + + mk.commit("/a", "+\"d\" : {}", mk.getHeadRevision(), null); + branchRevisionId = mk.commit("/a", "+\"e\" : {}", branchRevisionId, null); + + rs.gc(); + + branchRevisionId = mk.commit("/a", "+\"f\" : {}", branchRevisionId, null); + mk.merge(branchRevisionId, null); + + rs.gc(); + + String history = mk.getRevisionHistory(Long.MIN_VALUE, Integer.MIN_VALUE, null); + assertEquals(1, parseJSONArray(history).size()); + } + + /** + * Verify garbage collection can run concurrently with commits. + * + * @throws Exception if an error occurs + */ + @Test + public void testConcurrentGC() throws Exception { + ScheduledExecutorService gcExecutor = Executors.newScheduledThreadPool(1); + gcExecutor.scheduleWithFixedDelay(new Runnable() { + @Override + public void run() { + rs.gc(); + } + }, 100, 20, TimeUnit.MILLISECONDS); + + mk.commit("/", "+\"a\" : { \"b\" : { \"c\" : { \"d\" : {} } } }", + mk.getHeadRevision(), null); + + try { + for (int i = 0; i < 20; i++) { + mk.commit("/a/b/c/d", "+\"e\" : {}", mk.getHeadRevision(), null); + Thread.sleep(10); + mk.commit("/a/b/c/d/e", "+\"f\" : {}", mk.getHeadRevision(), null); + Thread.sleep(30); + mk.commit("/a/b/c/d", "-\"e\"", mk.getHeadRevision(), null); + } + } finally { + gcExecutor.shutdown(); + } + } + + /** + * Verify garbage collection can run concurrently with branch & merge. + * + * @throws Exception if an error occurs + */ + @Test + @Ignore + public void testConcurrentMergeGC() throws Exception { + ScheduledExecutorService gcExecutor = Executors.newScheduledThreadPool(1); + gcExecutor.scheduleWithFixedDelay(new Runnable() { + @Override + public void run() { + rs.gc(); + } + }, 100, 20, TimeUnit.MILLISECONDS); + + mk.commit("/", "+\"a\" : { \"b\" : { \"c\" : { \"d\" : {} } } }", + mk.getHeadRevision(), null); + + try { + for (int i = 0; i < 20; i++) { + String branchId = mk.branch(mk.getHeadRevision()); + if ((i & 1) == 0) { + /* add some data in even runs */ + branchId = mk.commit("/a/b/c/d", "+\"e\" : {}", branchId, null); + Thread.sleep(10); + branchId = mk.commit("/a/b/c/d/e", "+\"f\" : {}", branchId, null); + } else { + /* remove added data in odd runs */ + branchId = mk.commit("/a/b/c/d", "-\"e\"", branchId, null); + } + Thread.sleep(30); + mk.merge(branchId, null); + } + } finally { + gcExecutor.shutdown(); + } + } + + /** + * Parses the provided string into a {@code JSONArray}. + * + * @param json string to be parsed + * @return a {@code JSONArray} + * @throws {@code AssertionError} if the string cannot be parsed into a {@code JSONArray} + */ + private JSONArray parseJSONArray(String json) throws AssertionError { + JSONParser parser = new JSONParser(); + try { + Object obj = parser.parse(json); + assertTrue(obj instanceof JSONArray); + return (JSONArray) obj; + } catch (Exception e) { + throw new AssertionError("not a valid JSON array: " + e.getMessage()); + } + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/CommitGateTest.java b/oak-mk/src/test/java/org/apache/jackrabbit/mk/util/CommitGateIT.java similarity index 87% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/util/CommitGateTest.java rename to oak-mk/src/test/java/org/apache/jackrabbit/mk/util/CommitGateIT.java index 3f4cf827e92..80c1369c8c7 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/CommitGateTest.java +++ b/oak-mk/src/test/java/org/apache/jackrabbit/mk/util/CommitGateIT.java @@ -23,7 +23,7 @@ /** * Test the commit gate. */ -public class CommitGateTest extends TestCase { +public class CommitGateIT extends TestCase { public void test() throws InterruptedException { final CommitGate gate = new CommitGate(); @@ -59,7 +59,7 @@ public void run() { t.start(); } Thread.sleep(waitMillis * 10); - assertTrue(threadCount < spurious.get()); + // assertTrue(threadCount < spurious.get()); <- depends on timing assertEquals(10, tick.get()); tick.set(0); spurious.set(0); @@ -72,8 +72,9 @@ public void run() { for (Thread j : threads) { j.join(); } - assertTrue("ticks: " + tick.get() + " min: " + threadCount * commitCount + " spurious: " + spurious.get(), - tick.get() >= threadCount * commitCount * 0.2 && tick.get() <= threadCount * commitCount * 1.2); + // disabled: depends on timing + // assertTrue("ticks: " + tick.get() + " min: " + threadCount * commitCount + " spurious: " + spurious.get(), + // tick.get() >= threadCount * commitCount * 0.2 && tick.get() <= threadCount * commitCount * 1.2); } } diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/IOUtilsTest.java b/oak-mk/src/test/java/org/apache/jackrabbit/mk/util/IOUtilsTest.java similarity index 93% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/util/IOUtilsTest.java rename to oak-mk/src/test/java/org/apache/jackrabbit/mk/util/IOUtilsTest.java index b6730271f81..99767eb6bd3 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/IOUtilsTest.java +++ b/oak-mk/src/test/java/org/apache/jackrabbit/mk/util/IOUtilsTest.java @@ -197,8 +197,22 @@ public void testVarLong() throws IOException { assertEquals(-1, in.read()); } + public void testVarEOF() throws IOException { + try { + IOUtils.readVarInt(new ByteArrayInputStream(new byte[0])); + fail(); + } catch (EOFException e) { + // expected + } + try { + IOUtils.readVarLong(new ByteArrayInputStream(new byte[0])); + fail(); + } catch (EOFException e) { + // expected + } + } - private void testVarInt(int x, int expectedLen) throws IOException { + private static void testVarInt(int x, int expectedLen) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); IOUtils.writeVarInt(out, x); byte[] data = out.toByteArray(); @@ -212,7 +226,7 @@ private void testVarInt(int x, int expectedLen) throws IOException { assertEquals(-1, in.read()); } - private void testVarLong(long x, int expectedLen) throws IOException { + private static void testVarLong(long x, int expectedLen) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); IOUtils.writeVarLong(out, x); byte[] data = out.toByteArray(); diff --git a/oak-mk/src/test/java/org/apache/jackrabbit/mk/util/MicroKernelInputStreamTest.java b/oak-mk/src/test/java/org/apache/jackrabbit/mk/util/MicroKernelInputStreamTest.java new file mode 100644 index 00000000000..88f4bf6193f --- /dev/null +++ b/oak-mk/src/test/java/org/apache/jackrabbit/mk/util/MicroKernelInputStreamTest.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law + * or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.apache.jackrabbit.mk.util; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Random; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Tests the {@code MicroKernelInputStream}. + */ +public class MicroKernelInputStreamTest { + + MicroKernel mk = new MicroKernelImpl(); + + @Test + public void small() throws IOException { + doTest(10, 10); + } + + @Test + public void medium() throws IOException { + doTest(1000, 10); + } + + @Test + public void large() throws IOException { + doTest(100000, 1); + } + + private void doTest(int maxLength, int count) throws IOException { + String[] s = new String[count * 2]; + Random r = new Random(0); + for (int i = 0; i < s.length;) { + int len = count == 1 ? maxLength : r.nextInt(maxLength); + byte[] data = new byte[len]; + r.nextBytes(data); + s[i++] = mk.write(new ByteArrayInputStream(data)); + s[i++] = mk.write(new ByteArrayInputStream(data)); + } + r.setSeed(0); + for (int i = 0; i < s.length;) { + int len = count == 1 ? maxLength : r.nextInt(maxLength); + byte[] expectedData = new byte[len]; + r.nextBytes(expectedData); + assertEquals(len, mk.getLength(s[i++])); + + String id = s[i++]; + doTestReadFully(expectedData, len, id); + doTestRead(expectedData, len, id); + } + } + + private void doTestReadFully(byte[] expectedData, int expectedLen, String id) + throws IOException { + byte[] got = MicroKernelInputStream.readFully(mk, id); + assertByteArrayEquals(expectedData, expectedLen, got); + } + + private static void assertByteArrayEquals(byte[] expected, int expectedLen, byte[] got) { + assertEquals(expectedLen, got.length); + for (int j = 0; j < expectedLen; j++) { + if (expected[j] != got[j]) { + assertEquals("j:" + j, expected[j], got[j]); + } + } + } + + private void doTestRead(byte[] expectedData, int expectedLen, String id) throws IOException { + InputStream in = new MicroKernelInputStream(mk, id); + Random r = new Random(1); + ByteArrayOutputStream buff = new ByteArrayOutputStream(); + int minLen = 0; + if (expectedLen > 1000000) { + minLen = 4000; + } + int pos = 0; + while (true) { + int op = r.nextInt(5); + if (op == 0) { + // read one byte + int x = in.read(); + if (x < 0) { + break; + } + buff.write(x); + pos++; + } else if (op == 1) { + // skip a large number of bytes + long n = minLen + r.nextInt(5000); + long skipped = in.skip(n); + assertTrue(skipped >= 0); + buff.write(expectedData, pos, (int) skipped); + pos += skipped; + } else if (op == 2) { + // skip a small number of bytes (possibly negative) + long n = r.nextInt(10) - 3; + long skipped = in.skip(n); + assertTrue(skipped >= 0); + buff.write(expectedData, pos, (int) skipped); + pos += skipped; + } else if (op == 3) { + // read a large number of bytes + byte[] x = new byte[minLen + r.nextInt(5000)]; + int l = in.read(x); + if (l < 0) { + break; + } + buff.write(x, 0, l); + pos += l; + } else { + // read a small number of bytes + int offset = r.nextInt(10); + int len = minLen + r.nextInt(1000); + byte[] x = new byte[offset + len]; + int l = in.read(x, offset, len); + if (l < 0) { + break; + } + buff.write(x, offset, l); + pos += l; + } + } + byte[] got = buff.toByteArray(); + assertByteArrayEquals(expectedData, expectedLen, got); + } + +} diff --git a/oak-mk/src/test/java/org/apache/jackrabbit/mk/util/NameFilterTest.java b/oak-mk/src/test/java/org/apache/jackrabbit/mk/util/NameFilterTest.java new file mode 100644 index 00000000000..153c04997bb --- /dev/null +++ b/oak-mk/src/test/java/org/apache/jackrabbit/mk/util/NameFilterTest.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mk.util; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Tests the NameFilter utility class. + */ +public class NameFilterTest { + + @Test + public void test() { + NameFilter filter = new NameFilter(new String[]{"foo*", "-foo99"}); + assertTrue(filter.matches("foo1")); + assertTrue(filter.matches("foo*")); + assertTrue(filter.matches("foo bar")); + assertTrue(filter.matches("foo 99")); + assertFalse(filter.matches("foo99")); + assertTrue(filter.containsWildcard()); + + filter = new NameFilter(new String[]{"*foo"}); + assertTrue(filter.matches("foo")); + assertTrue(filter.matches("-123foo")); + assertFalse(filter.matches("bar")); + assertTrue(filter.containsWildcard()); + + filter = new NameFilter(new String[]{"foo\\*bar"}); + assertFalse(filter.matches("foo bar")); + assertTrue(filter.matches("foo*bar")); + assertFalse(filter.containsWildcard()); + + filter = new NameFilter(new String[]{"foo\\bar"}); + assertTrue(filter.matches("foo\\bar")); + assertFalse(filter.containsWildcard()); + + filter = new NameFilter(new String[]{"foo\\"}); + assertTrue(filter.matches("foo\\")); + assertFalse(filter.containsWildcard()); + + filter = new NameFilter(new String[]{"*"}); + assertTrue(filter.matches("*")); + assertTrue(filter.matches("\\*")); + assertTrue(filter.matches("blah")); + assertTrue(filter.containsWildcard()); + + filter = new NameFilter(new String[]{"\\*"}); + assertTrue(filter.matches("*")); + assertFalse(filter.matches("\\*")); + assertFalse(filter.matches("blah")); + assertFalse(filter.containsWildcard()); + + filter = new NameFilter(new String[]{"\\- topic"}); + assertTrue(filter.matches("- topic")); + assertFalse(filter.containsWildcard()); + + filter = new NameFilter(new String[]{"*", "- topic"}); + assertFalse(filter.matches(" topic")); + assertTrue(filter.matches("- topic")); + assertTrue(filter.matches("blah")); + assertTrue(filter.containsWildcard()); + + filter = new NameFilter(new String[]{"foo\\-bar"}); + assertFalse(filter.matches("foo-bar")); + assertTrue(filter.matches("foo\\-bar")); + assertFalse(filter.containsWildcard()); + + filter = new NameFilter(new String[]{"foo\\\\*bar"}); + assertTrue(filter.matches("foo\\*bar")); + assertFalse(filter.matches("foo\\ blah bar")); + assertFalse(filter.matches("foo*bar")); + assertFalse(filter.containsWildcard()); + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/StopWatch.java b/oak-mk/src/test/java/org/apache/jackrabbit/mk/util/StopWatch.java similarity index 100% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/util/StopWatch.java rename to oak-mk/src/test/java/org/apache/jackrabbit/mk/util/StopWatch.java diff --git a/oak-core/src/test/java/org/apache/jackrabbit/mk/util/StringUtilsTest.java b/oak-mk/src/test/java/org/apache/jackrabbit/mk/util/StringUtilsTest.java similarity index 100% rename from oak-core/src/test/java/org/apache/jackrabbit/mk/util/StringUtilsTest.java rename to oak-mk/src/test/java/org/apache/jackrabbit/mk/util/StringUtilsTest.java diff --git a/oak-mk/src/test/resources/logback-test.xml b/oak-mk/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..e92401c93ea --- /dev/null +++ b/oak-mk/src/test/resources/logback-test.xml @@ -0,0 +1,39 @@ + + + + + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + target/unit-tests.log + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + + + + + diff --git a/oak-mongomk-perf/pom.xml b/oak-mongomk-perf/pom.xml new file mode 100644 index 00000000000..739ebaf67f0 --- /dev/null +++ b/oak-mongomk-perf/pom.xml @@ -0,0 +1,93 @@ + + + + + + 4.0.0 + + + org.apache.jackrabbit + oak-parent + 0.6-SNAPSHOT + ../oak-parent/pom.xml + + + oak-mongomk-perf + Oak Mongo MicroKernel Performance Test + + + + + + org.apache.jackrabbit + oak-mongomk + ${project.version} + + + log4j + log4j + 1.2.16 + + + commons-cli + commons-cli + 1.2 + + + com.jamonapi + jamon + 2.4 + + + junit + junit + + + + + + + + maven-assembly-plugin + + + jar-with-dependencies + + + + org.apache.jackrabbit.mongomk.perf.MicroKernelPerf + + + + + + make-assembly + package + + single + + + + + + + + diff --git a/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/BlobStoreFS.java b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/BlobStoreFS.java new file mode 100644 index 00000000000..45d3ec514bc --- /dev/null +++ b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/BlobStoreFS.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.mongomk.perf; + +import java.io.File; +import java.io.InputStream; +import org.apache.jackrabbit.mk.blobs.BlobStore; + + +public class BlobStoreFS implements BlobStore{ + + private final File rootDir; + + public BlobStoreFS(String rootPath) { + File rootDir = new File(rootPath); + if (!rootDir.isDirectory()) { + rootDir.mkdirs(); + } + + this.rootDir = rootDir; + } + + @Override + public long getBlobLength(String blobId) throws Exception { + return 0; + } + + @Override + public int readBlob(String blobId, long blobOffset, byte[] buffer, int bufferOffset, int length) throws Exception { + return 0; + } + + @Override + public String writeBlob(InputStream is) throws Exception { + return null; + } +} diff --git a/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/Config.java b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/Config.java new file mode 100644 index 00000000000..10492ac953c --- /dev/null +++ b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/Config.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.mongomk.perf; + +import java.util.Properties; + +public class Config { + + private static final String MASTER_HOST = "master.host"; + private static final String MASTER_PORT = "master.port"; + private static final String MONGO_DATABASE = "mongo.db"; + private static final String MONGO_HOST = "mongo.host"; + private static final String MONGO_PORT = "mongo.port"; + private final Properties properties; + + public Config(Properties properties) { + this.properties = properties; + } + + public String getMasterHost() { + return properties.getProperty(MASTER_HOST); + } + + public int getMasterPort() { + return Integer.parseInt(properties.getProperty(MASTER_PORT)); + } + + public String getMongoDatabase() { + return properties.getProperty(MONGO_DATABASE); + } + + public String getMongoHost() { + return properties.getProperty(MONGO_HOST); + } + + public int getMongoPort() { + return Integer.parseInt(properties.getProperty(MONGO_PORT)); + } +} diff --git a/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/MicroKernelPerf.java b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/MicroKernelPerf.java new file mode 100644 index 00000000000..9641aa10ac1 --- /dev/null +++ b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/MicroKernelPerf.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.mongomk.perf; + +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.Properties; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.GnuParser; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionBuilder; +import org.apache.commons.cli.Options; +import org.apache.log4j.PropertyConfigurator; + +public class MicroKernelPerf { + private static Config config; + private static boolean masterMode; + private static boolean prepareEnvironment; + + public static void main(String[] args) throws Exception { + configLog4J(null); + readConfig(); + + evalCommandLineOptions(args); + + if (prepareEnvironment) { + PrepareEnvironment prepare = new PrepareEnvironment(config); + prepare.start(); + } + + if (masterMode) { + MicroKernelPerfMaster master = new MicroKernelPerfMaster(config); + master.start(); + } else { + MicroKernelPerfClient client = new MicroKernelPerfClient(config); + client.start(); + } + } + + private static void configLog4J(String path) throws Exception { + InputStream is = null; + + if (path == null) { + is = MicroKernelPerfClient.class.getResourceAsStream("/log4j.cfg"); + } else { + is = new FileInputStream(path); + } + + Properties properties = new Properties(); + properties.load(is); + is.close(); + + PropertyConfigurator.configure(properties); + } + + @SuppressWarnings("static-access") + private static void evalCommandLineOptions(String[] args) throws Exception { + Option log4jOption = OptionBuilder.withLongOpt("log4j-path").hasArg().withArgName("path") + .withDescription("path to a log4j config file").create("log4j"); + Option masterOption = OptionBuilder.withLongOpt("master-mode").withDescription("starts this in master mode") + .create("master"); + Option prepareOption = OptionBuilder.withLongOpt("prepare-environment") + .withDescription("resets the environment before executing").create("prep"); + + Options options = new Options(); + options.addOption(log4jOption); + options.addOption(masterOption); + options.addOption(prepareOption); + + CommandLineParser parser = new GnuParser(); + CommandLine line = parser.parse(options, args); + + if (line.hasOption(log4jOption.getOpt())) { + configLog4J(line.getOptionValue(log4jOption.getOpt())); + } + if (line.hasOption(masterOption.getOpt())) { + masterMode = true; + } + if (line.hasOption(prepareOption.getOpt())) { + prepareEnvironment = true; + } + } + + private static void readConfig() throws Exception { + InputStream is = MicroKernelPerfClient.class.getResourceAsStream("/config.cfg"); + + Properties properties = new Properties(); + properties.load(is); + + is.close(); + + config = new Config(properties); + } +} diff --git a/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/MicroKernelPerfClient.java b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/MicroKernelPerfClient.java new file mode 100644 index 00000000000..a827a80c567 --- /dev/null +++ b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/MicroKernelPerfClient.java @@ -0,0 +1,217 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.perf; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.apache.jackrabbit.mk.blobs.BlobStore; +import org.apache.jackrabbit.mongomk.api.NodeStore; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.impl.MongoMicroKernel; +import org.apache.jackrabbit.mongomk.impl.NodeStoreMongo; +import org.apache.jackrabbit.mongomk.impl.json.DefaultJsopHandler; +import org.apache.jackrabbit.mongomk.impl.json.JsopParser; +import org.apache.jackrabbit.mongomk.perf.RandomJsopGenerator.RandomJsop; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.log4j.Logger; +import org.json.JSONObject; + +import com.jamonapi.Monitor; +import com.jamonapi.MonitorFactory; +import com.mongodb.BasicDBObject; +import com.mongodb.DBCollection; +import com.mongodb.WriteConcern; + +public class MicroKernelPerfClient { + + private static class Stats extends BasicDBObject { + private static final long serialVersionUID = -8819985408570757782L; + + private Stats(String id, double duration, long numOfCommits, long numOfNodes, long numOfObjects) { + put("id", id); + put("duration", duration); + put("numOfCommits", numOfCommits); + put("numOfNodes", numOfNodes); + put("numOfObjects", numOfObjects); + } + } + + private static class VerificationHandler extends DefaultJsopHandler { + + private final List addedNodes = new LinkedList(); + private final Map> addedProperties = new HashMap>(); + + @Override + public void nodeAdded(String parentPath, String name) { + addedNodes.add(PathUtils.concat(parentPath, name)); + } + + @Override + public void propertySet(String path, String key, Object value) { + List properties = addedProperties.get(path); + if (properties == null) { + properties = new LinkedList(); + addedProperties.put(path, properties); + } + properties.add(key); + } + } + + private static final Logger LOG = Logger.getLogger(MicroKernelPerfClient.class); + + private static final Logger PERF = Logger.getLogger("PERFORMANCE"); + + private static void assertNodeExists(String path, String node, JSONObject result) throws Exception { + if (!path.equals(node)) { + JSONObject temp = result; + for (String segment : PathUtils.elements(node)) { + temp = temp.getJSONObject(segment); + + if (temp == null) { + throw new Exception(String.format("The node %s could not be found!", node)); + } + } + } + } + + private static void assertPropertyExists(String path, String property, JSONObject result) throws Exception { + JSONObject temp = result; + for (String segment : PathUtils.elements(path)) { + temp = temp.optJSONObject(segment); + + if (temp == null) { + throw new Exception(String.format("The node %s could not be found!", path)); + } + } + + Object value = temp.opt(property); + if (value == null) { + throw new Exception(String.format("The node %s did not containt the property %s!", path, property)); + } + } + + private Monitor commitMonitor; + private final Config config; + private Monitor getNodesMonitor; + + private MongoMicroKernel microKernel; + + private MongoConnection mongoConnection; + + private RandomJsopGenerator randomJsopGenerator; + + public MicroKernelPerfClient(Config config) throws Exception { + this.config = config; + + initMongo(); + initMicroKernel(); + initRandomJsopGenerator(); + initMonitoring(); + } + + public void start() throws Exception { + LOG.info("Starting client..."); + + startCommitting(); + } + + private void createStats(VerificationHandler handler, JSONObject result) { + long numOfNodes = mongoConnection.getNodeCollection().count(); + long numOfCommits = mongoConnection.getCommitCollection().count(); + + Stats commitStats = new Stats("commit", commitMonitor.getLastValue(), numOfCommits, numOfNodes, + handler.addedNodes.size() + handler.addedProperties.size()); + + Stats getNodesStats = new Stats("getNodes", getNodesMonitor.getLastValue(), numOfCommits, numOfNodes, + numOfNodes - handler.addedNodes.size()); + + DBCollection statsCollection = mongoConnection.getDB().getCollection("statistics"); + statsCollection.insert(new Stats[] { commitStats, getNodesStats }, WriteConcern.NONE); + } + + private void initMicroKernel() throws Exception { + NodeStore nodeStore = new NodeStoreMongo(mongoConnection); + BlobStore blobStore = new BlobStoreFS(System.getProperty("java.io.tmpdir")); + + microKernel = new MongoMicroKernel(nodeStore, blobStore); + } + + private void initMongo() throws Exception { + mongoConnection = new MongoConnection(config.getMongoHost(), config.getMongoPort(), config.getMongoDatabase()); + } + + private void initMonitoring() { + commitMonitor = MonitorFactory.getTimeMonitor("commit"); + getNodesMonitor = MonitorFactory.getTimeMonitor("getNodes"); + } + + private void initRandomJsopGenerator() throws Exception { + randomJsopGenerator = new RandomJsopGenerator(); + } + + private void startCommitting() throws Exception { + while (true) { + RandomJsop randomJsop = randomJsopGenerator.nextRandom(); + + String commitPath = randomJsop.getPath(); + String jsonDiff = randomJsop.getJsop(); + String revisionId = null; + String message = randomJsop.getMessage(); + + commitMonitor.start(); + String newRevisionId = microKernel.commit(commitPath, jsonDiff, revisionId, message); + commitMonitor.stop(); + PERF.info(commitMonitor); + LOG.debug(String.format("Committed (%s): %s, %s\n%s", newRevisionId, commitPath, message, jsonDiff)); + + getNodesMonitor.start(); + String getPath = "".equals(commitPath) ? "/" : commitPath; + String json = microKernel.getNodes(getPath, newRevisionId, -1, 0, -1, null); + getNodesMonitor.stop(); + PERF.info(getNodesMonitor); + LOG.debug(String.format("GetNodes (%s: %s", newRevisionId, json)); + + VerificationHandler handler = new VerificationHandler(); + JsopParser jsopParser = new JsopParser(commitPath, jsonDiff, handler); + jsopParser.parse(); + + JSONObject result = new JSONObject(json); + + verify(handler, result, getPath); + createStats(handler, result); + + randomJsopGenerator.setSeed(getPath, json); + } + } + + private void verify(VerificationHandler handler, JSONObject result, String getPath) throws Exception { + for (String node : handler.addedNodes) { + assertNodeExists(getPath, node, result); + } + + for (Map.Entry> entry : handler.addedProperties.entrySet()) { + String path = entry.getKey(); + List properties = entry.getValue(); + for (String property : properties) { + assertPropertyExists(path, property, result); + } + } + } +} diff --git a/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/MicroKernelPerfMaster.java b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/MicroKernelPerfMaster.java new file mode 100644 index 00000000000..8517ccc2d9e --- /dev/null +++ b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/MicroKernelPerfMaster.java @@ -0,0 +1,254 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.perf; + +import java.util.LinkedList; +import java.util.List; + +import org.apache.jackrabbit.mk.blobs.BlobStore; +import org.apache.jackrabbit.mongomk.api.NodeStore; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.impl.MongoMicroKernel; +import org.apache.jackrabbit.mongomk.impl.NodeStoreMongo; +import org.apache.jackrabbit.mongomk.impl.json.DefaultJsopHandler; +import org.apache.jackrabbit.mongomk.impl.json.JsopParser; +import org.apache.jackrabbit.mongomk.model.CommitMongo; +import org.apache.jackrabbit.mongomk.model.SyncMongo; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.log4j.Logger; +import org.json.JSONArray; +import org.json.JSONObject; + +import com.mongodb.DBCollection; +import com.mongodb.DBCursor; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; + +public class MicroKernelPerfMaster { + + private class ContinousHandler extends DefaultJsopHandler { + private final JSONObject jsonObject; + + private ContinousHandler() { + this.jsonObject = new JSONObject(); + } + + @Override + public void nodeAdded(String parentPath, String name) { + try { + if (!PathUtils.denotesRoot(name)) { + JSONObject parent = this.getObjectByPath(parentPath); + if (!parent.has(name)) { + parent.put(name, new JSONObject()); + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void propertySet(String path, String key, Object value) { + try { + if (!PathUtils.denotesRoot(key)) { + JSONObject element = this.getObjectByPath(path); + element.put(key, value); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private JSONObject getObjectByPath(String path) throws Exception { + JSONObject jsonObject = this.jsonObject; + + if (!"".equals(path)) { + for (String segment : PathUtils.elements(path)) { + LOG.debug(String.format("Checking segment %s of path %s in object %s", segment, path, jsonObject)); + jsonObject = jsonObject.optJSONObject(segment); + if (jsonObject == null) { + throw new Exception(String.format("The path %s was not found in the current state", path)); + } + } + } + + return jsonObject; + } + } + + private static final Logger LOG = Logger.getLogger(MicroKernelPerfMaster.class); + private final Config config; + private ContinousHandler handler; + private long lastCommitRevId; + private long lastHeadRevId; + private long lastRevId; + private MongoMicroKernel microKernel; + private MongoConnection mongoConnection; + + public MicroKernelPerfMaster(Config config) throws Exception { + this.config = config; + + this.initMongo(); + this.initMicroKernel(); + this.initHandler(); + } + + public void start() throws Exception { + LOG.info("Starting server..."); + + this.startVerifying(); + } + + private void initHandler() { + this.handler = new ContinousHandler(); + } + + private void initMicroKernel() throws Exception { + NodeStore nodeStore = new NodeStoreMongo(this.mongoConnection); + BlobStore blobStore = new BlobStoreFS(System.getProperty("java.io.tmpdir")); + + this.microKernel = new MongoMicroKernel(nodeStore, blobStore); + } + + private void initMongo() throws Exception { + this.mongoConnection = new MongoConnection(this.config.getMongoHost(), this.config.getMongoPort(), + this.config.getMongoDatabase()); + } + + private void startVerifying() throws Exception { + while (true) { + List commitMongos = this.waitForCommit(); + for (CommitMongo commitMongo : commitMongos) { + if (commitMongo.isFailed()) { + LOG.info(String.format("Skipping commit %d because it failed", commitMongo.getRevisionId())); + this.lastRevId = commitMongo.getRevisionId(); + } else { + LOG.info(String.format("Verifying commit %d", commitMongo.getRevisionId())); + this.verifyCommit(commitMongo); + this.verifyCommitOrder(commitMongo); + this.lastRevId = commitMongo.getRevisionId(); + this.lastCommitRevId = commitMongo.getRevisionId(); + } + } + } + } + + private void verifyCommit(CommitMongo commitMongo) throws Exception { + String path = commitMongo.getPath(); + String jsop = commitMongo.getDiff(); + + JsopParser jsopParser = new JsopParser(path, jsop, this.handler); + jsopParser.parse(); + + String json = this.microKernel.getNodes("/", String.valueOf(commitMongo.getRevisionId()), -1, 0, -1, null); + JSONObject resultJson = new JSONObject(json); + + this.verifyEquality(this.handler.jsonObject, resultJson); + + LOG.info(String.format("Successfully verified commit %d", commitMongo.getRevisionId())); + } + + private void verifyCommitOrder(CommitMongo commitMongo) throws Exception { + long baseRevId = commitMongo.getBaseRevId(); + long revId = commitMongo.getRevisionId(); + if (baseRevId != this.lastCommitRevId) { + throw new Exception(String.format( + "Revision %d has a base revision of %d but last successful commit was %d", revId, baseRevId, + this.lastCommitRevId)); + } + } + + private void verifyEquality(JSONObject expected, JSONObject actual) throws Exception { + LOG.debug(String.format("Verifying for equality %s (expected) vs %s (actual)", expected, actual)); + + try { + if (expected.length() != (actual.length() - 1)) { // substract 1 b/c of :childCount + throw new Exception(String.format( + "Unequal number of children/properties: %d (expected) vs %d (actual)", expected.length(), + actual.length() - 1)); + } + + JSONArray expectedNames = expected.names(); + if (expectedNames != null) { + for (int i = 0; i < expectedNames.length(); ++i) { + String name = expectedNames.getString(i); + + Object expectedValue = expected.get(name); + Object actualValue = actual.get(name); + + if ((expectedValue instanceof JSONObject) && (actualValue instanceof JSONObject)) { + this.verifyEquality((JSONObject) expectedValue, (JSONObject) actualValue); + } else if ((expectedValue != null) && (actualValue != null)) { + if (!expectedValue.equals(actualValue)) { + throw new Exception(String.format( + "Key %s: Expected value '%s' does not macht actual value '%s'", name, + expectedValue, actualValue)); + } + } else if (expectedValue != null) { + throw new Exception(String.format( + "Key %s: Did not find an actual value for expected value '%s'", name, expectedValue)); + } else if (actualValue != null) { + throw new Exception(String.format( + "Key %s: Did not find an expected value for actual value '%s'", name, actualValue)); + } + } + } + } catch (Exception e) { + LOG.error( + String.format("Verificytion for equality failed: %s (expected) vs %s (actual)", expected, actual), + e); + throw e; + } + } + + private List waitForCommit() { + // TODO Change this to MicroKernel#waitForCommit + List commitMongos = new LinkedList(); + this.lastHeadRevId = 0L; + + while (true) { + LOG.debug("Waiting for commit..."); + + DBCollection headCollection = this.mongoConnection.getSyncCollection(); + SyncMongo syncMongo = (SyncMongo) headCollection.findOne(); + if (this.lastHeadRevId < syncMongo.getHeadRevisionId()) { + DBCollection commitCollection = this.mongoConnection.getCommitCollection(); + DBObject query = QueryBuilder.start(CommitMongo.KEY_REVISION_ID).greaterThan(this.lastRevId) + .and(CommitMongo.KEY_REVISION_ID).lessThanEquals(syncMongo.getHeadRevisionId()).get(); + DBObject sort = QueryBuilder.start(CommitMongo.KEY_REVISION_ID).is(1).get(); + DBCursor dbCursor = commitCollection.find(query).sort(sort); + while (dbCursor.hasNext()) { + commitMongos.add((CommitMongo) dbCursor.next()); + } + + if (commitMongos.size() > 0) { + LOG.debug(String.format("Found %d new commits", commitMongos.size())); + + break; + } + this.lastHeadRevId = syncMongo.getHeadRevisionId(); + } + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + // noop + } + } + + return commitMongos; + } +} diff --git a/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/PrepareEnvironment.java b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/PrepareEnvironment.java new file mode 100644 index 00000000000..fe0b45c269f --- /dev/null +++ b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/PrepareEnvironment.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.mongomk.perf; + +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.log4j.Logger; + +public class PrepareEnvironment { + + private static final Logger LOG = Logger.getLogger(PrepareEnvironment.class); + + private final Config config; + private MongoConnection mongoConnection; + + public PrepareEnvironment(Config config) throws Exception { + this.config = config; + initMongo(); + } + + public void start() { + LOG.info("Preparing environment"); + mongoConnection.initializeDB(true); + } + + private void initMongo() throws Exception { + mongoConnection = new MongoConnection(config.getMongoHost(), config.getMongoPort(), + config.getMongoDatabase()); + } +} diff --git a/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/RandomJsopGenerator.java b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/RandomJsopGenerator.java new file mode 100644 index 00000000000..d9f195f6cc9 --- /dev/null +++ b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/perf/RandomJsopGenerator.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.perf; + +import java.util.Random; +import java.util.UUID; + +import org.apache.jackrabbit.mk.json.JsopBuilder; +import org.apache.jackrabbit.mongomk.api.model.Node; +import org.apache.jackrabbit.mongomk.impl.builder.NodeBuilder; +import org.apache.jackrabbit.oak.commons.PathUtils; + +public class RandomJsopGenerator { + + public static class RandomJsop { + private final String jsop; + private final String message; + private final String path; + + public RandomJsop(String path, String jsop, String message) { + this.path = path; + this.jsop = jsop; + this.message = message; + } + + public String getJsop() { + return this.jsop; + } + + public String getMessage() { + return this.message; + } + + public String getPath() { + return this.path; + } + } + + private static final int OP_ADD_NODE = 0; + + private static final int OP_ADD_PROP = 1; + + public static void main(String[] args) throws Exception { + RandomJsopGenerator gen = new RandomJsopGenerator(); + for (int i = 0; i < 10; ++i) { + RandomJsop rand = gen.nextRandom(); + System.out.println(rand.path); + System.out.println(rand.jsop); + System.out.println(); + } + } + + private Node[] descendants; + + private String path; + + private Random random; + + public RandomJsopGenerator() throws Exception { + this.setSeed("", "{ \"/\" : {} }"); + } + + public RandomJsop nextRandom() { + JsopBuilder jsopBuilder = new JsopBuilder(); + + int numOps = this.random.nextInt(10) + 1; + for (int i = 0; i < numOps; ++i) { + if (this.createRandomOp(jsopBuilder)) { + jsopBuilder.newline(); + } else { + --i; + } + } + + return new RandomJsop(this.path, jsopBuilder.toString(), UUID.randomUUID().toString()); + } + + public void setSeed(String path, String json) throws Exception { + this.path = path; + String all = String.format("{ \"%s\" : %s }", PathUtils.getName(path), json); + Node node = NodeBuilder.build(all, path); + // FIXME - This needs to change to node.getChildNodeEntries(0, -1). + //this.descendants = node.getDescendants(false).toArray(new Node[0]); + this.random = new Random(); + } + + private boolean createRandomAddNodeOp(JsopBuilder jsopBuilder) { + Node random = this.selectRandom(); + + String childName = this.createRandomString(); + String newPath = PathUtils.concat(random.getPath(), childName); + String addPath = newPath; + if (!"".equals(this.path)) { + addPath = PathUtils.relativize(this.path, newPath); + } + + jsopBuilder.tag('+'); + jsopBuilder.key(addPath); + jsopBuilder.object(); + jsopBuilder.endObject(); + + return true; + } + + private boolean createRandomAddPropOp(JsopBuilder jsopBuilder) { + int next = this.random.nextInt(this.descendants.length); + Node random = this.descendants[next]; + String addPath = PathUtils.relativize(this.path, random.getPath()); + if ("".equals(addPath)) { + addPath = "/"; + } + + jsopBuilder.tag('+'); + jsopBuilder.key(addPath); + jsopBuilder.object(); + + int numProps = this.random.nextInt(10) + 1; + for (int i = 0; i < numProps; ++i) { + String propName = this.createRandomString(); + String propValue = this.createRandomString(); + + jsopBuilder.key(propName); + jsopBuilder.value(propValue); + } + + jsopBuilder.endObject(); + + return true; + } + + private boolean createRandomOp(JsopBuilder jsopBuilder) { + boolean performed = false; + + int op = this.random.nextInt(2); + + switch (op) { + case OP_ADD_NODE: { + performed = this.createRandomAddNodeOp(jsopBuilder); + break; + } + case OP_ADD_PROP: { + performed = this.createRandomAddPropOp(jsopBuilder); + break; + } + } + + return performed; + } + + private String createRandomString() { + int length = this.random.nextInt(6) + 5; + char[] chars = new char[length]; + for (int i = 0; i < length; ++i) { + char rand = (char) (this.random.nextInt(65) + 59); + if (Character.isLetterOrDigit(rand)) { + chars[i] = rand; + } else { + --i; + } + } + + return new String(chars); + } + + private Node selectRandom() { + Node randomNode = null; + + int next = this.random.nextInt(this.descendants.length); + randomNode = this.descendants[next]; + + return randomNode; + } +} diff --git a/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/performance/write/MultipleMksWriteNodesTest.java b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/performance/write/MultipleMksWriteNodesTest.java new file mode 100644 index 00000000000..67cdaeda31e --- /dev/null +++ b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/performance/write/MultipleMksWriteNodesTest.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.performance.write; + +import org.apache.jackrabbit.mongomk.impl.MongoMicroKernel; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + + +/** + * Writing tests with multiple Mks. + */ +public class MultipleMksWriteNodesTest extends MultipleNodesTestBase { + + static int mkNumber = 5; + static long nodesNumber=2000; + static SimpleWriter[] sWorker = new SimpleWriter[mkNumber]; + static AdvanceWriter[] aWorker = new AdvanceWriter[mkNumber]; + + @BeforeClass + public static void init() throws Exception { + readConfig(); + initMongo(); + for (int i = 0; i < mkNumber; i++) { + MongoMicroKernel mk = initMicroKernel(); + sWorker[i] = new SimpleWriter("Thread " + i, mk,nodesNumber); + aWorker[i] = new AdvanceWriter("Thread " + i, mk,nodesNumber); + } + } + + @Before + public void cleanDatabase() { + mongoConnection.initializeDB(true); + } + + /** + * Each worker creates 2000 nodes on the same level. + * 5 workers x 2000 nodes=10000 nodes + * @throws InterruptedException + */ + @Test + public void testWriteSameLine() throws InterruptedException { + + for (int i = 0; i < mkNumber; i++) { + sWorker[i].start(); + + } + for (int i = 0; i < mkNumber; i++) { + sWorker[i].join(); + } + } + + /** + * Each worker is creating a pyramid containing 2000 nodes. + * 5 workers x 2000 nodes=10000 nodes + * + * @throws InterruptedException + */ + @Test + public void testWritePyramid() throws InterruptedException { + + for (int i = 0; i < mkNumber; i++) { + aWorker[i].start(); + + } + for (int i = 0; i < mkNumber; i++) { + aWorker[i].join(); + } + } + +} + +class SimpleWriter extends Thread { + + MongoMicroKernel mk; + + long nodesNumber; + + public SimpleWriter(String str, MongoMicroKernel mk, long nodesNumber) { + super(str); + this.mk = mk; + this.nodesNumber = nodesNumber; + } + + public void run() { + for (int i = 0; i < nodesNumber; i++) { + TestUtil.createNode(mk, "/", getId() + "No" + i); + } + } +} + +class AdvanceWriter extends Thread { + + MongoMicroKernel mk; + long nodesNumber; + + public AdvanceWriter(String str, MongoMicroKernel mk, long nodesNumber) { + super(str); + this.mk = mk; + this.nodesNumber = nodesNumber; + } + + public void run() { + + TestUtil.insertNode(mk, "/", 0, 50, nodesNumber, "T" + getId()); + + } +} diff --git a/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/performance/write/MultipleNodesTestBase.java b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/performance/write/MultipleNodesTestBase.java new file mode 100644 index 00000000000..a60d68ba704 --- /dev/null +++ b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/performance/write/MultipleNodesTestBase.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.mongomk.performance.write; + +import java.io.InputStream; +import java.util.Properties; + +import org.apache.jackrabbit.mk.blobs.BlobStore; +import org.apache.jackrabbit.mongomk.api.NodeStore; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.impl.MongoMicroKernel; +import org.apache.jackrabbit.mongomk.impl.NodeStoreMongo; +import org.apache.jackrabbit.mongomk.perf.BlobStoreFS; +import org.apache.jackrabbit.mongomk.perf.Config; + +public class MultipleNodesTestBase { + + + protected static MongoConnection mongoConnection; + private static Config config; + + static void initMongo() throws Exception { + mongoConnection = new MongoConnection(config.getMongoHost(), + config.getMongoPort(), config.getMongoDatabase()); + } + + static MongoMicroKernel initMicroKernel() throws Exception { + NodeStore nodeStore = new NodeStoreMongo(mongoConnection); + BlobStore blobStore = new BlobStoreFS( + System.getProperty("java.io.tmpdir")); + return new MongoMicroKernel(nodeStore, blobStore); + } + + static void readConfig() throws Exception { + InputStream is = MultipleNodesTestBase.class + .getResourceAsStream("/config.cfg"); + Properties properties = new Properties(); + properties.load(is); + is.close(); + config = new Config(properties); + } +} diff --git a/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/performance/write/TestUtil.java b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/performance/write/TestUtil.java new file mode 100644 index 00000000000..576c3dc225d --- /dev/null +++ b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/performance/write/TestUtil.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.performance.write; + +import org.apache.jackrabbit.mk.api.MicroKernel; + +public class TestUtil { + + /** + * Recursively builds a pyramid tree structure. + * + * @param mk + * @param parentFolderName + * The path where the node will be added. + * @param index + * @param numberOfChildren + * Number of children. + * @param nodesNumber + * Number of nodes. + */ + static void insertNode(MicroKernel mk, String parentFolderName, int index, + int numberOfChildren, long nodesNumber, String nodePrefixName) { + // if all the nodes are on the same level + if (numberOfChildren == 0) { + for (long i = 0; i < nodesNumber; i++) { + createNode(mk, parentFolderName, nodePrefixName + i); + System.out.println("Created node " + i); + } + return; + } + + if (index >= nodesNumber) + return; + createNode(mk, parentFolderName, nodePrefixName + index); + for (int i = 1; i <= numberOfChildren; i++) { + if (!parentFolderName.endsWith("/")) + parentFolderName = parentFolderName + "/"; + insertNode(mk, parentFolderName + nodePrefixName + index, index + * numberOfChildren + i, numberOfChildren, nodesNumber, nodePrefixName); + } + + } + + /** + * Creates a new node. + * + * @param mk + * @param parentNode + * @param name + * @return + */ + public static String createNode(MicroKernel mk, String parentNode, String name) { + + return mk.commit(parentNode, "+\"" + name + "\" : {} \n", null, ""); + + } +} diff --git a/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/performance/write/WriteNodesTest.java b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/performance/write/WriteNodesTest.java new file mode 100644 index 00000000000..87ea63ad7d0 --- /dev/null +++ b/oak-mongomk-perf/src/main/java/org/apache/jackrabbit/mongomk/performance/write/WriteNodesTest.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.performance.write; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Measures the time needed for creating different tree node structures.Only one + * mongoMk is used for writing operation. + */ +public class WriteNodesTest extends MultipleNodesTestBase { + static MicroKernel mk; + + @BeforeClass + public static void init() throws Exception { + readConfig(); + initMongo(); + mk=initMicroKernel(); + } + + @Before + public void cleanDatabase() { + mongoConnection.initializeDB(true); + } + + /** + * Creates 10000 nodes, all with on the same level with the same parent + * node. + */ + @Test + public void addNodesInLine() { + int nodesNumber = 10000; + TestUtil.insertNode(mk, "/", 0, 0, nodesNumber, "N"); + } + + /** + * Creates 10000 nodes, all of them having 10 children nodes. + */ + @Test + public void addNodes10Children() { + int nodesNumber = 10000; + TestUtil.insertNode(mk, "/", 0, 10, nodesNumber, "N"); + } + + /** + * Creates 10000 nodes, all of them having 100 children nodes. + */ + @Test + public void addNodes100Children() { + int nodesNumber = 10000; + TestUtil.insertNode(mk, "/", 0, 100, nodesNumber, "N"); + } + + /** + * Creates 10000 nodes, all of them on different levels.Each node has one + * child only. + */ + @Test + public void addNodes1Child() { + int nodesNumber = 2000; + TestUtil.insertNode(mk, "/", 0, 1, nodesNumber,"N"); + } + +} diff --git a/oak-mongomk-perf/src/main/resources/config.cfg b/oak-mongomk-perf/src/main/resources/config.cfg new file mode 100644 index 00000000000..25858a177f8 --- /dev/null +++ b/oak-mongomk-perf/src/main/resources/config.cfg @@ -0,0 +1,21 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +master.host = localhost +master.port = 12345 + +mongo.host = localhost +mongo.port = 27017 +mongo.db = mk-tf \ No newline at end of file diff --git a/oak-mongomk-perf/src/main/resources/log4j.cfg b/oak-mongomk-perf/src/main/resources/log4j.cfg new file mode 100644 index 00000000000..015e4dd8b9f --- /dev/null +++ b/oak-mongomk-perf/src/main/resources/log4j.cfg @@ -0,0 +1,22 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +log4j.rootLogger=INFO, console + +log4j.appender.console=org.apache.log4j.ConsoleAppender +log4j.appender.console.layout=org.apache.log4j.PatternLayout +log4j.appender.console.layout.ConversionPattern=[%t] %-5p %c: %m%n + +log4j.logger.PERFORMANCE=INFO \ No newline at end of file diff --git a/oak-mongomk-test/pom.xml b/oak-mongomk-test/pom.xml new file mode 100644 index 00000000000..0d9d34079bf --- /dev/null +++ b/oak-mongomk-test/pom.xml @@ -0,0 +1,63 @@ + + + + + + 4.0.0 + + + org.apache.jackrabbit + oak-parent + 0.6-SNAPSHOT + ../oak-parent/pom.xml + + + oak-mongomk-test + Oak Mongo MicroKernel Test + + + + + + org.apache.jackrabbit + oak-mongomk + ${project.version} + test + + + + + org.apache.jackrabbit + oak-it-mk + ${project.version} + test + + + + ch.qos.logback + logback-classic + 1.0.1 + test + + + + + diff --git a/oak-mongomk-test/src/test/java/org/apache/jackrabbit/mongomk/test/it/MongoDataStoreIT.java b/oak-mongomk-test/src/test/java/org/apache/jackrabbit/mongomk/test/it/MongoDataStoreIT.java new file mode 100644 index 00000000000..834875f0d4f --- /dev/null +++ b/oak-mongomk-test/src/test/java/org/apache/jackrabbit/mongomk/test/it/MongoDataStoreIT.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.mongomk.test.it; + +import org.apache.jackrabbit.mk.test.DataStoreIT; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ + DataStoreIT.class, +}) +public class MongoDataStoreIT { +} diff --git a/oak-mongomk-test/src/test/java/org/apache/jackrabbit/mongomk/test/it/MongoEverythingIT.java b/oak-mongomk-test/src/test/java/org/apache/jackrabbit/mongomk/test/it/MongoEverythingIT.java new file mode 100644 index 00000000000..35e3373dc40 --- /dev/null +++ b/oak-mongomk-test/src/test/java/org/apache/jackrabbit/mongomk/test/it/MongoEverythingIT.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.mongomk.test.it; + +import org.apache.jackrabbit.mk.test.MicroKernelTestSuite; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ + MicroKernelTestSuite.class +}) +public class MongoEverythingIT { +} diff --git a/oak-mongomk-test/src/test/java/org/apache/jackrabbit/mongomk/test/it/MongoMicroKernelFixture.java b/oak-mongomk-test/src/test/java/org/apache/jackrabbit/mongomk/test/it/MongoMicroKernelFixture.java new file mode 100644 index 00000000000..7047127b4cf --- /dev/null +++ b/oak-mongomk-test/src/test/java/org/apache/jackrabbit/mongomk/test/it/MongoMicroKernelFixture.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.test.it; + +import java.io.InputStream; +import java.util.Properties; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.blobs.BlobStore; +import org.apache.jackrabbit.mk.test.MicroKernelFixture; +import org.apache.jackrabbit.mongomk.api.NodeStore; +import org.apache.jackrabbit.mongomk.impl.BlobStoreMongo; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.impl.MongoMicroKernel; +import org.apache.jackrabbit.mongomk.impl.NodeStoreMongo; +import org.junit.Assert; + +public class MongoMicroKernelFixture implements MicroKernelFixture { + + private static MongoConnection mongoConnection = createMongoConnection(); + + private static MongoConnection createMongoConnection() { + try { + InputStream is = MongoMicroKernelFixture.class.getResourceAsStream("/config.cfg"); + Properties properties = new Properties(); + properties.load(is); + + String host = properties.getProperty("mongo.host"); + int port = Integer.parseInt(properties.getProperty("mongo.port")); + String database = properties.getProperty("mongo.db"); + + return new MongoConnection(host, port, database); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void setUpCluster(MicroKernel[] cluster) { + try { + mongoConnection.initializeDB(true); + NodeStore nodeStore = new NodeStoreMongo(mongoConnection); + BlobStore blobStore = new BlobStoreMongo(mongoConnection); + + MicroKernel mk = new MongoMicroKernel(nodeStore, blobStore); + for (int i = 0; i < cluster.length; i++) { + cluster[i] = mk; + } + } catch (Exception e) { + Assert.fail(e.getMessage()); + } + } + + @Override + public void syncMicroKernelCluster(MicroKernel... nodes) { + } + + @Override + public void tearDownCluster(MicroKernel[] cluster) { + try { + mongoConnection.clearDB(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/oak-mongomk-test/src/test/java/org/apache/jackrabbit/mongomk/test/it/MongoMicroKernelIT.java b/oak-mongomk-test/src/test/java/org/apache/jackrabbit/mongomk/test/it/MongoMicroKernelIT.java new file mode 100644 index 00000000000..8bcdec2a8bf --- /dev/null +++ b/oak-mongomk-test/src/test/java/org/apache/jackrabbit/mongomk/test/it/MongoMicroKernelIT.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.mongomk.test.it; + +import org.apache.jackrabbit.mk.test.MicroKernelIT; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ + MicroKernelIT.class, +}) +public class MongoMicroKernelIT { +} diff --git a/oak-mongomk-test/src/test/resources/META-INF/services/org.apache.jackrabbit.mk.test.MicroKernelFixture b/oak-mongomk-test/src/test/resources/META-INF/services/org.apache.jackrabbit.mk.test.MicroKernelFixture new file mode 100644 index 00000000000..ab13ff8f6f3 --- /dev/null +++ b/oak-mongomk-test/src/test/resources/META-INF/services/org.apache.jackrabbit.mk.test.MicroKernelFixture @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +org.apache.jackrabbit.mongomk.test.it.MongoMicroKernelFixture diff --git a/oak-mongomk-test/src/test/resources/config.cfg b/oak-mongomk-test/src/test/resources/config.cfg new file mode 100644 index 00000000000..fea5763bb9c --- /dev/null +++ b/oak-mongomk-test/src/test/resources/config.cfg @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +mongo.host = 127.0.0.1 +mongo.port = 27017 +mongo.db = mk-tf \ No newline at end of file diff --git a/oak-mongomk-test/src/test/resources/logback-test.xml b/oak-mongomk-test/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..e92401c93ea --- /dev/null +++ b/oak-mongomk-test/src/test/resources/logback-test.xml @@ -0,0 +1,39 @@ + + + + + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + target/unit-tests.log + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + + + + + diff --git a/oak-mongomk/pom.xml b/oak-mongomk/pom.xml new file mode 100644 index 00000000000..32e6c233521 --- /dev/null +++ b/oak-mongomk/pom.xml @@ -0,0 +1,156 @@ + + + + + + 4.0.0 + + + org.apache.jackrabbit + oak-parent + 0.6-SNAPSHOT + ../oak-parent/pom.xml + + + oak-mongomk + Oak Mongo MicroKernel + bundle + + + + + org.apache.felix + maven-bundle-plugin + true + + + + org.apache.jackrabbit.mongomk.* + + + org.apache.jackrabbit.mongomk.api.*, + org.apache.jackrabbit.mk.model.*, + org.apache.jackrabbit.mk.store.*, + org.apache.jackrabbit.mk.persistence.* + + + org.apache.sling.commons.osgi;inline=org/apache/sling/commons/osgi/PropertiesUtil.class + + + + + + org.apache.felix + maven-scr-plugin + + + + + + + + + org.apache.jackrabbit + oak-mk + ${project.version} + + + + + org.mongodb + mongo-java-driver + 2.9.1 + + + + + org.slf4j + slf4j-api + 1.6.4 + + + + + commons-io + commons-io + 2.3 + + + commons-codec + commons-codec + 1.5 + + + org.json + json + 20090211 + + + + org.apache.sling + org.apache.sling.commons.osgi + 2.1.0 + true + + + + + org.osgi + org.osgi.core + provided + + + org.osgi + org.osgi.compendium + provided + + + biz.aQute + bndlib + provided + + + org.apache.felix + org.apache.felix.scr.annotations + provided + + + + + junit + junit + test + + + ch.qos.logback + logback-classic + 1.0.1 + test + + + com.googlecode.json-simple + json-simple + 1.1 + + + + + diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/BaseAction.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/BaseAction.java new file mode 100644 index 00000000000..05dc6949059 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/BaseAction.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.mongomk.action; + +import org.apache.jackrabbit.mongomk.impl.MongoConnection; + +/** + * An abstract base class for actions performed against {@code MongoDB}. + * + * @param The result type of the query. + */ +public abstract class BaseAction { + + protected MongoConnection mongoConnection; + + /** + * Constructs a new {@code AbstractAction}. + * + * @param mongoConnection The mongo connection. + */ + public BaseAction(MongoConnection mongoConnection) { + this.mongoConnection = mongoConnection; + } + + /** + * Executes this action. + * + * @return The result of the action. + * @throws Exception If an error occurred while executing the action. + */ + public abstract T execute() throws Exception; +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/FetchBranchBaseRevisionIdAction.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/FetchBranchBaseRevisionIdAction.java new file mode 100644 index 00000000000..86b21de245e --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/FetchBranchBaseRevisionIdAction.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.mongomk.action; + +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.model.CommitMongo; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBCollection; +import com.mongodb.DBCursor; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; + +/** + * An action for fetching the base (trunk) revision id that the branch is based on. + */ +public class FetchBranchBaseRevisionIdAction extends BaseAction { + + private final String branchId; + + /** + * Constructs a new {@code FetchBranchBaseRevisionIdAction}. + * + * @param mongoConnection The {@link MongoConnection}. + * @param branchId The branch id. It should not be null. + */ + public FetchBranchBaseRevisionIdAction(MongoConnection mongoConnection, String branchId) { + super(mongoConnection); + this.branchId = branchId; + } + + @Override + public Long execute() { + if (branchId == null) { + throw new IllegalArgumentException("Branch id cannot be null"); + } + + DBCollection commitCollection = mongoConnection.getCommitCollection(); + QueryBuilder queryBuilder = QueryBuilder.start(CommitMongo.KEY_FAILED) + .notEquals(Boolean.TRUE) + .and(CommitMongo.KEY_BRANCH_ID).is(branchId); + DBObject query = queryBuilder.get(); + + BasicDBObject filter = new BasicDBObject(); + filter.put(CommitMongo.KEY_BASE_REVISION_ID, 1); + + BasicDBObject orderBy = new BasicDBObject(CommitMongo.KEY_BASE_REVISION_ID, 1); + + DBCursor dbCursor = commitCollection.find(query, filter).sort(orderBy).limit(1); + if (dbCursor.hasNext()) { + CommitMongo commitMongo = (CommitMongo)dbCursor.next(); + return commitMongo.getBaseRevId(); + } + return 0L; + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/FetchCommitAction.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/FetchCommitAction.java new file mode 100644 index 00000000000..616d8b674d5 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/FetchCommitAction.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.action; + +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.model.CommitMongo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mongodb.DBCollection; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; + +/** + * An action for fetching a commit. An exception is thrown if a commit with the + * revision id does not exist. + */ +public class FetchCommitAction extends BaseAction { + + private static final Logger LOG = LoggerFactory.getLogger(FetchCommitAction.class); + + private final long revisionId; + + /** + * Constructs a new {@link FetchCommitAction} + * + * @param mongoConnection Mongo connection. + * @param revisionId Revision id. + */ + public FetchCommitAction(MongoConnection mongoConnection, long revisionId) { + super(mongoConnection); + this.revisionId = revisionId; + } + + @Override + public CommitMongo execute() throws Exception { + DBCollection commitCollection = mongoConnection.getCommitCollection(); + DBObject query = QueryBuilder.start(CommitMongo.KEY_FAILED).notEquals(Boolean.TRUE) + .and(CommitMongo.KEY_REVISION_ID).is(revisionId) + .get(); + + LOG.debug(String.format("Executing query: %s", query)); + + DBObject dbObject = commitCollection.findOne(query); + if (dbObject == null) { + throw new Exception(String.format("Commit with revision %d could not be found", revisionId)); + } + return (CommitMongo)dbObject; + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/FetchCommitsAction.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/FetchCommitsAction.java new file mode 100644 index 00000000000..a7c071e8083 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/FetchCommitsAction.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.action; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.model.CommitMongo; +import org.apache.jackrabbit.mongomk.model.NodeMongo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBCollection; +import com.mongodb.DBCursor; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; + +/** + * An action for fetching valid commits. + */ +public class FetchCommitsAction extends BaseAction> { + + private static final int LIMITLESS = -1; + private static final Logger LOG = LoggerFactory.getLogger(FetchCommitsAction.class); + + private long fromRevisionId = -1; + private long toRevisionId = -1; + private int maxEntries = LIMITLESS; + private boolean includeBranchCommits = true; + + /** + * Constructs a new {@link FetchCommitsAction} + * + * @param mongoConnection Mongo connection. + * @param toRevisionId To revision id. + */ + public FetchCommitsAction(MongoConnection mongoConnection) { + this(mongoConnection, -1L, -1L); + } + + /** + * Constructs a new {@link FetchCommitsAction} + * + * @param mongoConnection Mongo connection. + * @param toRevisionId To revision id. + */ + public FetchCommitsAction(MongoConnection mongoConnection, long toRevisionId) { + this(mongoConnection, -1L, toRevisionId); + } + + /** + * Constructs a new {@link FetchCommitsAction} + * + * @param mongoConnection Mongo connection. + * @param fromRevisionId From revision id. + * @param toRevisionId To revision id. + */ + public FetchCommitsAction(MongoConnection mongoConnection, long fromRevisionId, + long toRevisionId) { + super(mongoConnection); + this.fromRevisionId = fromRevisionId; + this.toRevisionId = toRevisionId; + } + + /** + * Sets the max number of entries that should be fetched. + * + * @param maxEntries The max number of entries. + */ + public void setMaxEntries(int maxEntries) { + this.maxEntries = maxEntries; + } + + /** + * Sets whether the branch commits are included in the query. + * + * @param includeBranchCommits Whether the branch commits are included. + */ + public void includeBranchCommits(boolean includeBranchCommits) { + this.includeBranchCommits = includeBranchCommits; + } + + @Override + public List execute() { + if (maxEntries == 0) { + return Collections.emptyList(); + } + DBCursor dbCursor = fetchListOfValidCommits(); + return convertToCommits(dbCursor); + } + + private DBCursor fetchListOfValidCommits() { + DBCollection commitCollection = mongoConnection.getCommitCollection(); + QueryBuilder queryBuilder = QueryBuilder.start(CommitMongo.KEY_FAILED).notEquals(Boolean.TRUE); + if (toRevisionId > -1) { + queryBuilder = queryBuilder.and(CommitMongo.KEY_REVISION_ID).lessThanEquals(toRevisionId); + } + + if (!includeBranchCommits) { + queryBuilder = queryBuilder.and(new BasicDBObject(NodeMongo.KEY_BRANCH_ID, + new BasicDBObject("$exists", false))); + } + + DBObject query = queryBuilder.get(); + + LOG.debug(String.format("Executing query: %s", query)); + + return maxEntries > 0? commitCollection.find(query).limit(maxEntries) : commitCollection.find(query); + } + + private List convertToCommits(DBCursor dbCursor) { + Map commits = new HashMap(); + while (dbCursor.hasNext()) { + CommitMongo commitMongo = (CommitMongo) dbCursor.next(); + commits.put(commitMongo.getRevisionId(), commitMongo); + } + + List validCommits = new LinkedList(); + if (commits.isEmpty()) { + return validCommits; + } + + Set revisions = commits.keySet(); + long currentRevision = (toRevisionId != -1 && revisions.contains(toRevisionId)) ? + toRevisionId : Collections.max(revisions); + + while (true) { + CommitMongo commitMongo = commits.get(currentRevision); + if (commitMongo == null) { + break; + } + validCommits.add(commitMongo); + long baseRevision = commitMongo.getBaseRevId(); + if (currentRevision == 0L || baseRevision < fromRevisionId) { + break; + } + currentRevision = baseRevision; + } + + LOG.debug(String.format("Found list of valid revisions for max revision %s: %s", + toRevisionId, validCommits)); + + return validCommits; + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/FetchHeadRevisionIdAction.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/FetchHeadRevisionIdAction.java new file mode 100644 index 00000000000..d3a738864c2 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/FetchHeadRevisionIdAction.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.mongomk.action; + +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.model.CommitMongo; +import org.apache.jackrabbit.mongomk.model.SyncMongo; + +import com.mongodb.DBCollection; + +/** + * An action for fetching the head revision. + */ +public class FetchHeadRevisionIdAction extends BaseAction { + + private boolean includeBranchCommits = true; + + /** + * Constructs a new {@code FetchHeadRevisionIdAction}. + * + * @param mongoConnection The {@link MongoConnection}. + */ + public FetchHeadRevisionIdAction(MongoConnection mongoConnection) { + super(mongoConnection); + } + + /** + * Sets whether the branch commits are included in the query. + * + * @param includeBranchCommits Whether the branch commits are included. + */ + public void includeBranchCommits(boolean includeBranchCommits) { + this.includeBranchCommits = includeBranchCommits; + } + + @Override + public Long execute() throws Exception { + DBCollection headCollection = mongoConnection.getSyncCollection(); + SyncMongo syncMongo = (SyncMongo)headCollection.findOne(); + long headRevisionId = syncMongo.getHeadRevisionId(); + if (includeBranchCommits) { + return headRevisionId; + } + + // Otherwise, find the first revision id that's not part of a branch. + long revisionId = headRevisionId; + while (true) { + CommitMongo commitMongo = new FetchCommitAction(mongoConnection, revisionId).execute(); + if (commitMongo.getBranchId() == null) { + return revisionId; + } + revisionId = commitMongo.getBaseRevId(); + } + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/FetchNodesAction.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/FetchNodesAction.java new file mode 100644 index 00000000000..1677e5663dd --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/FetchNodesAction.java @@ -0,0 +1,229 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.action; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.model.CommitMongo; +import org.apache.jackrabbit.mongomk.model.NodeMongo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBCollection; +import com.mongodb.DBCursor; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; + +/** + * An action for fetching nodes. + */ +public class FetchNodesAction extends BaseAction> { + + public static final int LIMITLESS_DEPTH = -1; + private static final Logger LOG = LoggerFactory.getLogger(FetchNodesAction.class); + + private final Set paths; + private final long revisionId; + + private String branchId; + private int depth = LIMITLESS_DEPTH; + private boolean fetchDescendants; + + /** + * Constructs a new {@code FetchNodesAction} to fetch a node and optionally + * its descendants under the specified path. + * + * @param mongoConnection The {@link MongoConnection}. + * @param path The path. + * @param fetchDescendants Determines whether the descendants of the path + * will be fetched as well. + * @param revisionId The revision id. + */ + public FetchNodesAction(MongoConnection mongoConnection, String path, + boolean fetchDescendants, long revisionId) { + super(mongoConnection); + paths = new HashSet(); + paths.add(path); + this.fetchDescendants = fetchDescendants; + this.revisionId = revisionId; + } + + /** + * Constructs a new {@code FetchNodesAction} to fetch nodes with the exact + * specified paths. + * + * @param mongoConnection The {@link MongoConnection}. + * @param path The exact paths to fetch nodes for. + * @param revisionId The revision id. + */ + public FetchNodesAction(MongoConnection mongoConnection, Set paths, + long revisionId) { + super(mongoConnection); + this.paths = paths; + this.revisionId = revisionId; + } + + + /** + * Sets the branchId for the query. + * + * @param branchId Branch id. + */ + public void setBranchId(String branchId) { + this.branchId = branchId; + } + + /** + * Sets the depth for the command. Only used when fetchDescendants is enabled. + * + * @param depth The depth for the command or -1 for limitless depth. + */ + public void setDepth(int depth) { + this.depth = depth; + } + + @Override + public List execute() { + if (paths.isEmpty()) { + return Collections.emptyList(); + } + DBCursor dbCursor = performQuery(); + List validCommits = new FetchCommitsAction(mongoConnection, revisionId).execute(); + return getMostRecentValidNodes(dbCursor, validCommits); + } + + private DBCursor performQuery() { + QueryBuilder queryBuilder = QueryBuilder.start(NodeMongo.KEY_PATH); + if (paths.size() > 1) { + queryBuilder = queryBuilder.in(paths); + } else { + String path = paths.toArray(new String[0])[0]; + if (fetchDescendants) { + Pattern pattern = createPrefixRegExp(path); + queryBuilder = queryBuilder.regex(pattern); + } else { + queryBuilder = queryBuilder.is(path); + } + } + + if (revisionId > 0) { + queryBuilder = queryBuilder.and(NodeMongo.KEY_REVISION_ID).lessThanEquals(revisionId); + } + + if (branchId == null) { + DBObject query = new BasicDBObject(NodeMongo.KEY_BRANCH_ID, new BasicDBObject("$exists", false)); + queryBuilder = queryBuilder.and(query); + } else { + // Not only return nodes in the branch but also nodes in the trunk + // before the branch was created. + FetchBranchBaseRevisionIdAction action = new FetchBranchBaseRevisionIdAction(mongoConnection, branchId); + long headBranchRevisionId = action.execute(); + + DBObject branchQuery = QueryBuilder.start().or( + QueryBuilder.start(NodeMongo.KEY_BRANCH_ID).is(branchId).get(), + QueryBuilder.start(NodeMongo.KEY_REVISION_ID).lessThanEquals(headBranchRevisionId).get() + ).get(); + queryBuilder = queryBuilder.and(branchQuery); + } + + DBObject query = queryBuilder.get(); + LOG.debug(String.format("Executing query: %s", query)); + + DBCollection nodeCollection = mongoConnection.getNodeCollection(); + return nodeCollection.find(query); + } + + private Pattern createPrefixRegExp(String path) { + StringBuilder sb = new StringBuilder(); + + if (depth < 0) { + sb.append("^"); + sb.append(path); + } else if (depth == 0) { + sb.append("^"); + sb.append(path); + sb.append("$"); + } else if (depth > 0) { + sb.append("^"); + if (!"/".equals(path)) { + sb.append(path); + } + sb.append("(/[^/]*)"); + sb.append("{0,"); + sb.append(depth); + sb.append("}$"); + } + + return Pattern.compile(sb.toString()); + } + + private List getMostRecentValidNodes(DBCursor dbCursor, + List validCommits) { + List validRevisions = extractRevisionIds(validCommits); + Map nodeMongos = new HashMap(); + + while (dbCursor.hasNext()) { + NodeMongo nodeMongo = (NodeMongo) dbCursor.next(); + + String path = nodeMongo.getPath(); + long revisionId = nodeMongo.getRevisionId(); + + LOG.debug(String.format("Converting node %s (%d)", path, revisionId)); + + if (!validRevisions.contains(revisionId)) { + LOG.debug(String.format("Node will not be converted as it is not a valid commit %s (%d)", + path, revisionId)); + continue; + } + + NodeMongo existingNodeMongo = nodeMongos.get(path); + if (existingNodeMongo != null) { + long existingRevId = existingNodeMongo.getRevisionId(); + + if (revisionId > existingRevId) { + nodeMongos.put(path, nodeMongo); + LOG.debug(String.format("Converted nodes was put into map and replaced %s (%d)", path, revisionId)); + } else { + LOG.debug(String.format("Converted nodes was not put into map because a newer version" + + " is available %s (%d)", path, revisionId)); + } + } else { + nodeMongos.put(path, nodeMongo); + LOG.debug("Converted node was put into map"); + } + } + + return new ArrayList(nodeMongos.values()); + } + + private List extractRevisionIds(List validCommits) { + List validRevisions = new ArrayList(validCommits.size()); + for (CommitMongo commitMongo : validCommits) { + validRevisions.add(commitMongo.getRevisionId()); + } + return validRevisions; + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/ReadAndIncHeadRevisionAction.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/ReadAndIncHeadRevisionAction.java new file mode 100644 index 00000000000..39efc6aab12 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/ReadAndIncHeadRevisionAction.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.mongomk.action; + +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.model.SyncMongo; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBCollection; +import com.mongodb.DBObject; + +/** + * An action for reading and incrementing the head revision id. + */ +public class ReadAndIncHeadRevisionAction extends BaseAction { + + /** + * Constructs a new {@code ReadAndIncHeadRevisionQuery}. + * + * @param mongoConnection The {@link MongoConnection}. + */ + public ReadAndIncHeadRevisionAction(MongoConnection mongoConnection) { + super(mongoConnection); + } + + @Override + public SyncMongo execute() throws Exception { + DBObject query = new BasicDBObject(); + DBObject inc = new BasicDBObject(SyncMongo.KEY_NEXT_REVISION_ID, 1L); + DBObject update = new BasicDBObject("$inc", inc); + DBCollection headCollection = mongoConnection.getSyncCollection(); + + DBObject dbObject = headCollection.findAndModify(query, null, null, false, update, true, false); + // Not sure why but sometimes dbObject is null. Simply retry for now. + while (dbObject == null) { + dbObject = headCollection.findAndModify(query, null, null, false, update, true, false); + } + return SyncMongo.fromDBObject(dbObject); + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/SaveAndSetHeadRevisionAction.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/SaveAndSetHeadRevisionAction.java new file mode 100644 index 00000000000..b25b0f47ff9 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/SaveAndSetHeadRevisionAction.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.mongomk.action; + +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.model.SyncMongo; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBCollection; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; + +/** + * An action for saving and setting the head revision id. + */ +public class SaveAndSetHeadRevisionAction extends BaseAction { + + private final long newHeadRevision; + private final long oldHeadRevision; + + /** + * Constructs a new {@code SaveAndSetHeadRevisionAction}. + * + * @param mongoConnection The {@link MongoConnection}. + * @param oldHeadRevision + * @param newHeadRevision + */ + public SaveAndSetHeadRevisionAction(MongoConnection mongoConnection, + long oldHeadRevision, long newHeadRevision) { + super(mongoConnection); + this.oldHeadRevision = oldHeadRevision; + this.newHeadRevision = newHeadRevision; + } + + @Override + public SyncMongo execute() throws Exception { + DBCollection headCollection = mongoConnection.getSyncCollection(); + DBObject query = QueryBuilder.start(SyncMongo.KEY_HEAD_REVISION_ID).is(oldHeadRevision).get(); + DBObject update = new BasicDBObject("$set", new BasicDBObject(SyncMongo.KEY_HEAD_REVISION_ID, newHeadRevision)); + DBObject dbObject = headCollection.findAndModify(query, null, null, false, update, true, false); + return SyncMongo.fromDBObject(dbObject); + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/SaveCommitAction.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/SaveCommitAction.java new file mode 100644 index 00000000000..208d0438ac7 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/SaveCommitAction.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.mongomk.action; + +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.model.CommitMongo; + +import com.mongodb.DBCollection; +import com.mongodb.WriteResult; + +/** + * An action for saving a commit. + */ +public class SaveCommitAction extends BaseAction { + + private final CommitMongo commitMongo; + + /** + * Constructs a new {@code SaveCommitAction}. + * + * @param mongoConnection The {@link MongoConnection}. + * @param commitMongo The {@link CommitMongo} to save. + */ + public SaveCommitAction(MongoConnection mongoConnection, CommitMongo commitMongo) { + super(mongoConnection); + this.commitMongo = commitMongo; + } + + @Override + public Boolean execute() throws Exception { + DBCollection commitCollection = mongoConnection.getCommitCollection(); + WriteResult writeResult = commitCollection.insert(commitMongo); + if (writeResult.getError() != null) { + throw new Exception(String.format("Insertion wasn't successful: %s", writeResult)); + } + return Boolean.TRUE; + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/SaveNodesAction.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/SaveNodesAction.java new file mode 100644 index 00000000000..d14c0212dcc --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/action/SaveNodesAction.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.mongomk.action; + +import java.util.Collection; + +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.model.NodeMongo; + +import com.mongodb.DBCollection; +import com.mongodb.DBObject; +import com.mongodb.WriteConcern; +import com.mongodb.WriteResult; + +/** + * An action for saving a list of nodes. + */ +public class SaveNodesAction extends BaseAction { + + private final Collection nodeMongos; + + /** + * Constructs a new {@code SaveNodesAction}. + * + * @param mongoConnection The {@link MongoConnection}. + * @param nodeMongos The list of {@link NodeMongo}s. + */ + public SaveNodesAction(MongoConnection mongoConnection, Collection nodeMongos) { + super(mongoConnection); + this.nodeMongos = nodeMongos; + } + + @Override + public Boolean execute() throws Exception { + DBCollection nodeCollection = mongoConnection.getNodeCollection(); + DBObject[] temp = nodeMongos.toArray(new DBObject[nodeMongos.size()]); + WriteResult writeResult = nodeCollection.insert(temp, WriteConcern.SAFE); + if ((writeResult != null) && (writeResult.getError() != null)) { + throw new Exception(String.format("Insertion wasn't successful: %s", writeResult)); + } + return Boolean.TRUE; + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/NodeStore.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/NodeStore.java new file mode 100644 index 00000000000..628513bf37d --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/NodeStore.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.api; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.api.MicroKernelException; +import org.apache.jackrabbit.mongomk.api.model.Commit; +import org.apache.jackrabbit.mongomk.api.model.Node; + +/** + * The NodeStore interface deals with all node related operations + * of the {@code MicroKernel}. + * + *

    + * Since binary storage and node storage most likely use different back-end + * technologies two separate interfaces for these operations are provided. + *

    + * + *

    + * This interface is not only a partly {@code MicroKernel} but also provides a + * different layer of abstraction by converting the {@link String} parameters + * into higher level objects to ease the development for implementors of the + * {@code MicroKernel}. + *

    + * + * @see {@code BlobStore} + */ +public interface NodeStore { + + /** + * @see MicroKernel#commit(String, String, String, String) + * + * @param commit The {@link Commit} object to store in the back-end. + * @return The revision id of this commit. + * @throws Exception If an error occurred while committing. + */ + String commit(Commit commit) throws Exception; + + /** + * @see MicroKernel#diff(String, String, String, int) + * + * @param fromRevisionId a revision id, if {@code null} the current head revision is assumed + * @param toRevisionId another revision id, if {@code null} the current head revision is assumed + * @param path optional path filter; if {@code null} or {@code ""}. + * The default ({@code "/"}) will be assumed, i.e. no filter will be applied + * @param depth Depth limit; if {@code -1} no limit will be applied + * @return JSON diff representation of the changes + * @throws MicroKernelException if any of the specified revisions doesn't exist or if another error occurs + */ + String diff(String fromRevisionId, String toRevisionId, String path, int depth) + throws Exception; + + /** + * @see MicroKernel#getHeadRevision() + * + * @return The revision id of the head revision. + * @throws Exception If an error occurred while retrieving the head revision. + */ + String getHeadRevision() throws Exception; + + /** + * @see MicroKernel#getJournal(String, String, String) + * + * @param fromRevisionId id of first revision to be returned in journal + * @param toRevisionId id of last revision to be returned in journal, + * if {@code null} the current head revision is assumed + * @param path optional path filter; if {@code null} or {@code ""} + * the default ({@code "/"}) will be assumed, i.e. no filter will be applied + * @return a chronological list of revisions in JSON format + * @throws Exception if an error occurred while getting the journal. + */ + String getJournal(String fromRevisionId, String toRevisionId, String path) + throws Exception; + + /** + * @see MicroKernel#getRevisionHistory(long, int, String) + * + * @param since timestamp (ms) of earliest revision to be returned + * @param maxEntries maximum #entries to be returned; if < 0, no limit will be applied. + * @param path optional path filter; if {@code null} or {@code ""} the default + * ({@code "/"}) will be assumed, i.e. no filter will be applied + * @return a list of revisions in chronological order in JSON format. + * @throws Exception if an error occurred while getting the revision history. + */ + String getRevisionHistory(long since, int maxEntries, String path) throws Exception;; + + /** + * @see MicroKernel#getNodes(String, String, int, long, int, String) + * + * @param path The path of the root of nodes to retrieve. + * @param revisionId The revision id of the nodes or {@code null} if the latest head revision + * should be retrieved. + * @param depth The maximum depth of the retrieved node tree or -1 to retrieve all nodes. + * @param offset The offset of the child list to retrieve. + * @param maxChildNodes The count of children to retrieve or -1 to retrieve all children. + * @param filter An optional filter for the retrieved nodes. + * @return The {@link Node} of the root node. + * @throws Exception If an error occurred while retrieving the nodes. + */ + Node getNodes(String path, String revisionId, int depth, long offset, int maxChildNodes, + String filter) throws Exception; + + /** + * @see MicroKernel#merge(String, String) + * + * @param branchRevisionId Branch revision id to merge. + * @param message Merge message. + * @return The revision id after merge. + * @throws Exception If an error occurred while merging. + */ + String merge(String branchRevisionId, String message) throws Exception; + + /** + * @see MicroKernel#nodeExists(String, String) + * + * @param path The path of the node to test. + * @param revisionId The revision id of the node or {@code null} for the head revision. + * @return {@code true} if the node for the specific revision exists else {@code false}. + * @throws Exception If an error occurred while testing the node. + */ + boolean nodeExists(String path, String revisionId) throws Exception; + + /** + * @see MicroKernel#waitForCommit(String, long) + * + * @param oldHeadRevisionId id of earlier head revision + * @param timeout the maximum time to wait in milliseconds + * @return the id of the head revision + * @throws Exception if an error occurred while waiting. + */ + String waitForCommit(String oldHeadRevisionId, long timeout) throws Exception; +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/command/Command.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/command/Command.java new file mode 100644 index 00000000000..464e579c3a9 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/command/Command.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.api.command; + + +/** + * The {@code Command} framework provides an way to encapsulate specific actions + * of the {code MicroKernel}. + * + *

    + * It adds some functionality for retries and other non business logic related + * actions (i.e. logging, performance tracking, etc). + *

    + * + * @see Command Pattern + * @see CommandExecutor + * + * @param The result type of the {@code Command}. + */ +public interface Command { + + /** + * Executes the {@code Command} and returns its result. + * + * @return The result. + * @throws Exception If an error occurred while executing. + */ + T execute() throws Exception; + + /** + * Returns the number of retries this {@code Command} should be retried in + * case of an error or false result. + * + *

    + * The number of reties is evaluated in the following way: + *

  • n < 0: Unlimited retries
  • + *
  • n = 0: No retries (just one execution)
  • + *
  • n > 0: Corresponding number of retries
  • + *

    + * + *

    + * In order to determine whether the {@code Command} should be retired on + * {@link #needsRetry(Exception)} or {@link #needsRetry(Object)} will be called. + *

    + * + * @see #needsRetry(Exception) + * @see #needsRetry(Object) + * + * @return The number of retries. + */ + int getNumOfRetries(); + + /** + * Will be called in case of an {@link Exception} during the execution and + * a given number of retries which has not exceeded. + * + * @param e The Exception which was thrown. + * @return {@code true} if a retry should be performed, else {@code false}. + */ + boolean needsRetry(Exception e); + + /** + * Will be called in case of a successful execution and a given number of + * retries which has not exceeded. + * + *

    + * This gives the implementor a chance to retry a false result. + *

    + * + * @param result The result of the execution. + * @return {@code true} if a retry should be performed, else {@code false}. + */ + boolean needsRetry(T result); +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/command/CommandExecutor.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/command/CommandExecutor.java new file mode 100644 index 00000000000..731e29317bb --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/command/CommandExecutor.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.mongomk.api.command; + +/** + * The executor part of the Command Pattern. + * + *

    + * The implementation of this class contains the business logic to execute a command. + *

    + * + * @see Command Pattern + * @see Command + */ +public interface CommandExecutor { + + /** + * Executes the given {@link Command} and returns the result. + * + *

    + * If an retry behavior is specified this will be taken care of by the implementation as well. + *

    + * + * @param command + * @return The result of the execution. + * @throws Exception If an error occurred while executing. + */ + T execute(Command command) throws Exception; +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/command/package-info.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/command/package-info.java new file mode 100644 index 00000000000..89634ae7f53 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/command/package-info.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. + */ + +/** + * Oak repository API + */ +@Version("0.1") +@Export(optional = "provide:=true") +package org.apache.jackrabbit.mongomk.api.command; + +import aQute.bnd.annotation.Export; +import aQute.bnd.annotation.Version; + diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/instruction/Instruction.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/instruction/Instruction.java new file mode 100644 index 00000000000..f8045296fca --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/instruction/Instruction.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.api.instruction; + + +/** + * FIXME - Remove other interfaces and renamed AddNodeInstructionImpl into + * AddNodeInstruction etc. + * + * An {@code Instruction} is an abstraction of a single + * JSOP operation. + * + *

    + * Each operation is a concrete sub-interface of {@code Instruction} and extending + * it by the specific properties of the operation. There is no exact 1 : 1 mapping + * between a {@code JSOP} operation and a sub-interface, i.e. in {@code JSOP} there + * is one add operation for adding nodes and properties whereas there are two specific + * sub-interfaces; one for adding a node and one for adding a property. + *

    + */ +public interface Instruction { + + /** + * Accepts an {@code InstructionVisitor}. + * + * @param visitor The visitor. + */ + void accept(InstructionVisitor visitor); + + /** + * Returns the path of this {@code Instruction}. + * + *

    + * The semantics of this property differ depending on the concrete subinterface. + *

    + * + * @return The path. + */ + String getPath(); + + /** + * The add node operation => "+" STRING ":" (OBJECT). + */ + public interface AddNodeInstruction extends Instruction { + } + + /** + * The copy node operation => "*" STRING ":" STRING + */ + public interface CopyNodeInstruction extends Instruction { + + /** + * Returns the destination path. + * + * @return the destination path. + */ + String getDestPath(); + + /** + * Returns the source path. + * + * @return the source path. + */ + String getSourcePath(); + } + + /** + * The move node operation => ">" STRING ":" STRING + */ + public interface MoveNodeInstruction extends Instruction { + + /** + * Returns the destination path. + * + * @return the destination path. + */ + String getDestPath(); + + /** + * Returns the source path. + * + * @return the source path. + */ + String getSourcePath(); + } + + /** + * The remove node operation => "-" STRING + */ + public interface RemoveNodeInstruction extends Instruction { + } + + /** + * The set property operation => "^" STRING ":" ATOM | ARRAY + */ + public interface SetPropertyInstruction extends Instruction { + + /** + * Returns the key of the property to set. + * + * @return The key. + */ + String getKey(); + + /** + * Returns the value of the property to set. + * + * @return The value. + */ + Object getValue(); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/instruction/InstructionVisitor.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/instruction/InstructionVisitor.java new file mode 100644 index 00000000000..4183864f6c4 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/instruction/InstructionVisitor.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.api.instruction; + +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.AddNodeInstruction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.CopyNodeInstruction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.MoveNodeInstruction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.RemoveNodeInstruction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.SetPropertyInstruction; + +/** + * A Visitor to iterate + * through a list of {@code Instruction}s without the need to use {@code instanceof} + * on each item. + */ +public interface InstructionVisitor { + + /** + * Visits a {@code AddNodeInstruction}. + * + * @param instruction The instruction. + */ + void visit(AddNodeInstruction instruction); + + /** + * Visits a {@code CopyNodeInstruction}. + * + * @param instruction The instruction. + */ + void visit(CopyNodeInstruction instruction); + + /** + * Visits a {@code MoveNodeInstruction}. + * + * @param instruction The instruction. + */ + void visit(MoveNodeInstruction instruction); + + /** + * Visits a {@code RemoveNodeInstruction}. + * + * @param instruction The instruction. + */ + void visit(RemoveNodeInstruction instruction); + + /** + * Visits a {@code SetPropertyInstruction}. + * + * @param instruction The instruction. + */ + void visit(SetPropertyInstruction instruction); +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/model/Commit.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/model/Commit.java new file mode 100644 index 00000000000..b088ff14ff8 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/model/Commit.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.api.model; + +import java.util.List; + +import org.apache.jackrabbit.mongomk.api.instruction.Instruction; + +/** + * A higher level object representing a commit. + */ +public interface Commit { + + /** + * Returns the private branch id the commit is under or {@code null} if the + * commit is in the public branch. + * + * @return The private branch id or {@code null} + */ + String getBranchId(); + + /** + * Returns the base revision id the commit is based on. + * + * @return The base revision id the commit is based on. + */ + Long getBaseRevisionId(); + + /** + * Returns the JSOP + * diff of this commit. + * + * @return The {@link String} representing the diff. + */ + String getDiff(); + + /** + * Returns the {@link List} of {@link Instruction}s which were created from + * the diff. + * + * @see #getDiff() + * + * @return The {@link List} of {@link Instruction}s. + */ + List getInstructions(); + + /** + * Returns the message of the commit. + * + * @return The message. + */ + String getMessage(); + + /** + * Returns the path of the root node of this commit. + * + * @return The path of the root node. + */ + String getPath(); + + /** + * Returns the revision id of this commit if known already, else this will + * return {@code null}. The revision id will be determined only after the + * commit has been successfully performed. + * + * @return The revision id of this commit or {@code null}. + */ + Long getRevisionId(); + + /** + * Sets the revision id of this commit. + * + * @param revisionId The revision id to set. + */ + void setRevisionId(Long revisionId); + + /** + * Returns the timestamp of this commit. + * + * @return The timestamp of this commit. + */ + long getTimestamp(); +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/model/Node.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/model/Node.java new file mode 100644 index 00000000000..126484c14e5 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/model/Node.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.api.model; + +import java.util.Iterator; +import java.util.Map; + +import org.apache.jackrabbit.mk.model.NodeDiffHandler; + +/** + * A higher level object representing a node. + */ +public interface Node { + + /** + * Returns the descendant node entry (descendant) + * + * @param name Name of the descendant. + * @return Descendant node. + */ + Node getChildNodeEntry(String name); + + /** + * + * Returns the total number of children of this node. + * + *

    + * This is not necessarily equal to the number of children returned by + * {@link #getChildren()} since this {@code Node} might be created with only + * a subset of children. + *

    + * + * @return The total number of children. + */ + int getChildNodeCount(); + + /** + * Returns the children iterator for the supplied offset and count. + * + * @param offset The offset to return the children from. + * @param count The number of children to return. + * @return Iterator with child entries. + */ + Iterator getChildNodeEntries(int offset, int count); + + /** + * Returns the properties this {@code Node} was created with. + * + * @return The properties. + */ + Map getProperties(); + + /** + * Diffs this node with the other node and calls the passed in diff handler. + * + * @param otherNode Other node. + * @param nodeDiffHandler Diff handler. + */ + void diff(Node otherNode, NodeDiffHandler nodeDiffHandler); + + /** + * Returns the path of this {@code Node}. + * + * @return The path. + */ + String getPath(); + + /** + * Returns the revision id of this node if known already, else this will return {@code null}. + * The revision id will be determined only after the commit has been successfully + * performed or the node has been read as part of an existing revision. + * + * @return The revision id of this commit or {@code null}. + */ + Long getRevisionId(); +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/model/package-info.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/model/package-info.java new file mode 100644 index 00000000000..50c6791e50c --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/model/package-info.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. + */ + +/** + * Oak repository API + */ +@Version("0.1") +@Export(optional = "provide:=true") +package org.apache.jackrabbit.mongomk.api.model; + +import aQute.bnd.annotation.Export; +import aQute.bnd.annotation.Version; + diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/package-info.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/package-info.java new file mode 100644 index 00000000000..afe5d948853 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/api/package-info.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. + */ + +/** + * Oak repository API + */ +@Version("0.1") +@Export(optional = "provide:=true") +package org.apache.jackrabbit.mongomk.api; + +import aQute.bnd.annotation.Export; +import aQute.bnd.annotation.Version; + diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/command/exception/ConflictingCommitException.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/command/exception/ConflictingCommitException.java new file mode 100644 index 00000000000..b8a1016fc15 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/command/exception/ConflictingCommitException.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.command.exception; + +public class ConflictingCommitException extends Exception { + + private static final long serialVersionUID = -5827664000083665577L; + + public ConflictingCommitException() { + super(); + } + + public ConflictingCommitException(String message) { + super(message); + } + + public ConflictingCommitException(Throwable t) { + super(t); + } + + public ConflictingCommitException(String message, Throwable t) { + super(message, t); + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/command/exception/InconsistentNodeHierarchyException.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/command/exception/InconsistentNodeHierarchyException.java new file mode 100644 index 00000000000..cec7f2475b8 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/command/exception/InconsistentNodeHierarchyException.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.command.exception; + +public class InconsistentNodeHierarchyException extends Exception { + + private static final long serialVersionUID = 6361719178625761034L; + +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/BlobStoreMongo.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/BlobStoreMongo.java new file mode 100644 index 00000000000..5055c6a985e --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/BlobStoreMongo.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.mongomk.impl; + +import java.io.InputStream; + +import org.apache.jackrabbit.mk.blobs.BlobStore; +import org.apache.jackrabbit.mongomk.api.command.Command; +import org.apache.jackrabbit.mongomk.api.command.CommandExecutor; +import org.apache.jackrabbit.mongomk.impl.command.DefaultCommandExecutor; +import org.apache.jackrabbit.mongomk.impl.command.GetBlobLengthCommand; +import org.apache.jackrabbit.mongomk.impl.command.ReadBlobCommand; +import org.apache.jackrabbit.mongomk.impl.command.WriteBlobCommand; + +public class BlobStoreMongo implements BlobStore { + + private final MongoConnection mongoConnection; + private final CommandExecutor commandExecutor; + + public BlobStoreMongo(MongoConnection mongoConnection) { + this.mongoConnection = mongoConnection; + commandExecutor = new DefaultCommandExecutor(); + } + + @Override + public long getBlobLength(String blobId) throws Exception { + Command command = new GetBlobLengthCommand(mongoConnection, blobId); + return commandExecutor.execute(command); + } + + @Override + public int readBlob(String blobId, long blobOffset, byte[] buffer, int bufferOffset, int length) throws Exception { + Command command = new ReadBlobCommand(mongoConnection, blobId, blobOffset, buffer, bufferOffset, length); + return commandExecutor.execute(command); + } + + @Override + public String writeBlob(InputStream is) throws Exception { + Command command = new WriteBlobCommand(mongoConnection, is); + return commandExecutor.execute(command); + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/MongoConnection.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/MongoConnection.java new file mode 100644 index 00000000000..30a7a8f0354 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/MongoConnection.java @@ -0,0 +1,195 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl; + +import java.util.Arrays; + +import org.apache.jackrabbit.mongomk.model.CommitMongo; +import org.apache.jackrabbit.mongomk.model.SyncMongo; +import org.apache.jackrabbit.mongomk.model.NodeMongo; + +import com.mongodb.BasicDBObject; +import com.mongodb.DB; +import com.mongodb.DBCollection; +import com.mongodb.DBObject; +import com.mongodb.Mongo; +import com.mongodb.gridfs.GridFS; + +/** + * The {@code MongoConnection} contains connection properties for the {@code MongoDB}. + */ +public class MongoConnection { + + public static final String INITIAL_COMMIT_MESSAGE = "This is an autogenerated initial commit"; + public static final String INITIAL_COMMIT_PATH = ""; + public static final String INITIAL_COMMIT_DIFF = "+\"/\" : {}"; + + private static final String COLLECTION_COMMITS = "commits"; + private static final String COLLECTION_NODES = "nodes"; + private static final String COLLECTION_SYNC = "sync"; + + private final DB db; + private final GridFS gridFS; + private final Mongo mongo; + + /** + * Constructs a new {@code MongoConnection}. + * + * @param host The host address. + * @param port The port. + * @param database The database name. + * @throws Exception If an error occurred while trying to connect. + */ + public MongoConnection(String host, int port, String database) throws Exception { + mongo = new Mongo(host, port); + db = mongo.getDB(database); + gridFS = new GridFS(db); + } + + /** + * Initializes the underlying DB. + * + * @param force If true, clears the DB before initializing the collections. + * Use with caution. + */ + public void initializeDB(boolean force) { + if (force) { + clearDB(); + } + initCommitCollection(force); + initNodeCollection(force); + initSyncCollection(force); + } + + /** + * Drops the collections of the underlying DB. + */ + public void clearDB() { + getNodeCollection().drop(); + getCommitCollection().drop(); + getSyncCollection().drop(); + } + + /** + * Closes the underlying Mongo instance + */ + public void close(){ + if (mongo != null){ + mongo.close(); + } + } + + /** + * Returns the commit {@link DBCollection}. + * + * @return The commit {@link DBCollection}. + */ + public DBCollection getCommitCollection() { + DBCollection commitCollection = db.getCollection(COLLECTION_COMMITS); + commitCollection.setObjectClass(CommitMongo.class); + return commitCollection; + } + + /** + * Returns the {@link DB}. + * + * @return The {@link DB}. + */ + public DB getDB() { + return db; + } + + /** + * Returns the {@link GridFS}. + * + * @return The {@link GridFS}. + */ + public GridFS getGridFS() { + return gridFS; + } + + /** + * Returns the sync {@link DBCollection}. + * + * @return The sync {@link DBCollection}. + */ + public DBCollection getSyncCollection() { + DBCollection syncCollection = db.getCollection(COLLECTION_SYNC); + syncCollection.setObjectClass(SyncMongo.class); + return syncCollection; + } + + /** + * Returns the node {@link DBCollection}. + * + * @return The node {@link DBCollection}. + */ + public DBCollection getNodeCollection() { + DBCollection nodeCollection = db.getCollection(COLLECTION_NODES); + nodeCollection.setObjectClass(NodeMongo.class); + return nodeCollection; + } + + private void initCommitCollection(boolean force) { + if (!force && db.collectionExists(MongoConnection.COLLECTION_COMMITS)){ + return; + } + DBCollection commitCollection = getCommitCollection(); + DBObject index = new BasicDBObject(); + index.put(CommitMongo.KEY_REVISION_ID, 1L); + DBObject options = new BasicDBObject(); + options.put("unique", Boolean.TRUE); + commitCollection.ensureIndex(index, options); + CommitMongo commit = new CommitMongo(); + commit.setAffectedPaths(Arrays.asList(new String[] { "/" })); + commit.setBaseRevId(0L); + commit.setDiff(INITIAL_COMMIT_DIFF); + commit.setMessage(INITIAL_COMMIT_MESSAGE); + commit.setPath(INITIAL_COMMIT_PATH); + commit.setRevisionId(0L); + commitCollection.insert(commit); + } + + private void initNodeCollection(boolean force) { + if (!force && db.collectionExists(MongoConnection.COLLECTION_NODES)){ + return; + } + DBCollection nodeCollection = getNodeCollection(); + DBObject index = new BasicDBObject(); + index.put(NodeMongo.KEY_PATH, 1L); + index.put(NodeMongo.KEY_REVISION_ID, 1L); + DBObject options = new BasicDBObject(); + options.put("unique", Boolean.TRUE); + nodeCollection.ensureIndex(index, options); + NodeMongo root = new NodeMongo(); + root.setRevisionId(0L); + root.setPath("/"); + nodeCollection.insert(root); + } + + private void initSyncCollection(boolean force) { + if (!force && db.collectionExists(MongoConnection.COLLECTION_SYNC)){ + return; + } + DBCollection headCollection = getSyncCollection(); + SyncMongo headMongo = new SyncMongo(); + headMongo.setHeadRevisionId(0L); + headMongo.setNextRevisionId(1L); + headCollection.insert(headMongo); + } + +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/MongoMicroKernel.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/MongoMicroKernel.java new file mode 100644 index 00000000000..de9cc5b04bf --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/MongoMicroKernel.java @@ -0,0 +1,220 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl; + +import java.io.InputStream; +import java.util.UUID; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.api.MicroKernelException; +import org.apache.jackrabbit.mk.blobs.BlobStore; +import org.apache.jackrabbit.mk.json.JsopBuilder; +import org.apache.jackrabbit.mk.util.NodeFilter; +import org.apache.jackrabbit.mongomk.api.NodeStore; +import org.apache.jackrabbit.mongomk.api.model.Commit; +import org.apache.jackrabbit.mongomk.api.model.Node; +import org.apache.jackrabbit.mongomk.impl.json.JsonUtil; +import org.apache.jackrabbit.mongomk.impl.model.CommitBuilder; +import org.apache.jackrabbit.mongomk.impl.model.CommitImpl; +import org.apache.jackrabbit.mongomk.impl.model.tree.MongoNodeState; + +/** + * The {@code MongoDB} implementation of the {@link MicroKernel}. + * + *

    + * This class will transform and delegate to instances of {@link NodeStore} and + * {@link BlobStore}. + *

    + */ +public class MongoMicroKernel implements MicroKernel { + + private final BlobStore blobStore; + private final NodeStore nodeStore; + + /** + * Constructs a new {@code MongoMicroKernel}. + * + * @param nodeStore The {@link NodeStore}. + * @param blobStore The {@link BlobStore}. + */ + public MongoMicroKernel(NodeStore nodeStore, BlobStore blobStore) { + this.nodeStore = nodeStore; + this.blobStore = blobStore; + } + + @Override + public String branch(String trunkRevisionId) throws MicroKernelException { + String revId = trunkRevisionId == null ? getHeadRevision() : trunkRevisionId; + + try { + CommitImpl commit = (CommitImpl)CommitBuilder.build("", + "", revId, MongoConnection.INITIAL_COMMIT_MESSAGE); + commit.setBranchId(UUID.randomUUID().toString()); + return nodeStore.commit(commit); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + @Override + public String commit(String path, String jsonDiff, String revisionId, String message) throws MicroKernelException { + try { + Commit commit = CommitBuilder.build(path, jsonDiff, revisionId, message); + return nodeStore.commit(commit); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + @Override + public String diff(String fromRevisionId, String toRevisionId, String path, + int depth) throws MicroKernelException { + try { + return nodeStore.diff(fromRevisionId, toRevisionId, path, depth); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + @Override + public long getChildNodeCount(String path, String revisionId) + throws MicroKernelException { + Node node; + try { + node = nodeStore.getNodes(path, revisionId, 0, 0, -1, null); + } catch (Exception e) { + throw new MicroKernelException(e); + } + if (node != null) { + return node.getChildNodeCount(); + } else { + throw new MicroKernelException("Path " + path + " not found in revision " + + revisionId); + } + } + + @Override + public String getHeadRevision() throws MicroKernelException { + try { + return nodeStore.getHeadRevision(); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + @Override + public String getJournal(String fromRevisionId, String toRevisionId, + String path) throws MicroKernelException { + try { + return nodeStore.getJournal(fromRevisionId, toRevisionId, path); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + @Override + public long getLength(String blobId) throws MicroKernelException { + try { + return blobStore.getBlobLength(blobId); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + @Override + public String getNodes(String path, String revisionId, int depth, long offset, + int maxChildNodes, String filter) throws MicroKernelException { + + NodeFilter nodeFilter = filter == null || filter.isEmpty() ? null : NodeFilter.parse(filter); + if (offset > 0 && nodeFilter != null && nodeFilter.getChildNodeFilter() != null) { + // Both an offset > 0 and a filter on node names have been specified... + throw new IllegalArgumentException("offset > 0 with child node filter"); + } + + try { + // FIXME Should filter, offset, and maxChildNodes be handled in Mongo instead? + Node rootNode = nodeStore.getNodes(path, revisionId, depth, offset, maxChildNodes, filter); + if (rootNode == null) { + return null; + } + + JsopBuilder builder = new JsopBuilder().object(); + JsonUtil.toJson(builder, new MongoNodeState(rootNode), depth, (int)offset, maxChildNodes, true, nodeFilter); + return builder.endObject().toString(); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + @Override + public String getRevisionHistory(long since, int maxEntries, String path) + throws MicroKernelException { + try { + return nodeStore.getRevisionHistory(since, maxEntries, path); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + @Override + public String merge(String branchRevisionId, String message) + throws MicroKernelException { + try { + return nodeStore.merge(branchRevisionId, message); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + @Override + public boolean nodeExists(String path, String revisionId) throws MicroKernelException { + try { + return nodeStore.nodeExists(path, revisionId); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + @Override + public int read(String blobId, long pos, byte[] buff, int off, int length) + throws MicroKernelException { + try { + return blobStore.readBlob(blobId, pos, buff, off, length); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + @Override + public String waitForCommit(String oldHeadRevisionId, long timeout) + throws MicroKernelException, InterruptedException { + try { + return nodeStore.waitForCommit(oldHeadRevisionId, timeout); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + + @Override + public String write(InputStream in) throws MicroKernelException { + try { + return blobStore.writeBlob(in); + } catch (Exception e) { + throw new MicroKernelException(e); + } + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/NodeStoreMongo.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/NodeStoreMongo.java new file mode 100644 index 00000000000..a5e13bfc954 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/NodeStoreMongo.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl; + +import org.apache.jackrabbit.mongomk.action.FetchCommitAction; +import org.apache.jackrabbit.mongomk.api.NodeStore; +import org.apache.jackrabbit.mongomk.api.command.Command; +import org.apache.jackrabbit.mongomk.api.command.CommandExecutor; +import org.apache.jackrabbit.mongomk.api.model.Commit; +import org.apache.jackrabbit.mongomk.api.model.Node; +import org.apache.jackrabbit.mongomk.impl.command.CommitCommand; +import org.apache.jackrabbit.mongomk.impl.command.DefaultCommandExecutor; +import org.apache.jackrabbit.mongomk.impl.command.DiffCommand; +import org.apache.jackrabbit.mongomk.impl.command.GetHeadRevisionCommand; +import org.apache.jackrabbit.mongomk.impl.command.GetJournalCommand; +import org.apache.jackrabbit.mongomk.impl.command.GetNodesCommand; +import org.apache.jackrabbit.mongomk.impl.command.GetRevisionHistoryCommand; +import org.apache.jackrabbit.mongomk.impl.command.MergeCommand; +import org.apache.jackrabbit.mongomk.impl.command.NodeExistsCommand; +import org.apache.jackrabbit.mongomk.impl.command.WaitForCommitCommand; +import org.apache.jackrabbit.mongomk.model.CommitMongo; +import org.apache.jackrabbit.mongomk.util.MongoUtil; + +/** + * Implementation of {@link NodeStore} for the {@code MongoDB}. + */ +public class NodeStoreMongo implements NodeStore { + + private final CommandExecutor commandExecutor; + private final MongoConnection mongoConnection; + + /** + * Constructs a new {@code NodeStoreMongo}. + * + * @param mongoConnection The {@link MongoConnection}. + */ + public NodeStoreMongo(MongoConnection mongoConnection) { + this.mongoConnection = mongoConnection; + commandExecutor = new DefaultCommandExecutor(); + } + + @Override + public String commit(Commit commit) throws Exception { + Command command = new CommitCommand(mongoConnection, commit); + Long revisionId = commandExecutor.execute(command); + return MongoUtil.fromMongoRepresentation(revisionId); + } + + @Override + public String diff(String fromRevision, String toRevision, String path, int depth) + throws Exception { + Command command = new DiffCommand(mongoConnection, + fromRevision, toRevision, path, depth); + return commandExecutor.execute(command); + } + + @Override + public String getHeadRevision() throws Exception { + GetHeadRevisionCommand command = new GetHeadRevisionCommand(mongoConnection); + long revisionId = commandExecutor.execute(command); + return MongoUtil.fromMongoRepresentation(revisionId); + } + + @Override + public Node getNodes(String path, String revisionId, int depth, long offset, + int maxChildNodes, String filter) throws Exception { + GetNodesCommand command = new GetNodesCommand(mongoConnection, path, + MongoUtil.toMongoRepresentation(revisionId)); + command.setBranchId(getBranchId(revisionId)); + command.setDepth(depth); + return commandExecutor.execute(command); + } + + @Override + public String merge(String branchRevisionId, String message) throws Exception { + MergeCommand command = new MergeCommand(mongoConnection, + branchRevisionId, message); + return commandExecutor.execute(command); + } + + @Override + public boolean nodeExists(String path, String revisionId) throws Exception { + NodeExistsCommand command = new NodeExistsCommand(mongoConnection, path, + MongoUtil.toMongoRepresentation(revisionId)); + String branchId = getBranchId(revisionId); + command.setBranchId(branchId); + return commandExecutor.execute(command); + } + + @Override + public String getJournal(String fromRevisionId, String toRevisionId, String path) + throws Exception { + GetJournalCommand command = new GetJournalCommand(mongoConnection, + fromRevisionId, toRevisionId, path); + return commandExecutor.execute(command); + } + + @Override + public String getRevisionHistory(long since, int maxEntries, String path) + throws Exception { + GetRevisionHistoryCommand command = new GetRevisionHistoryCommand(mongoConnection, + since, maxEntries, path); + return commandExecutor.execute(command); + } + + @Override + public String waitForCommit(String oldHeadRevisionId, long timeout) throws Exception { + WaitForCommitCommand command = new WaitForCommitCommand(mongoConnection, + oldHeadRevisionId, timeout); + long revisionId = commandExecutor.execute(command); + return MongoUtil.fromMongoRepresentation(revisionId); + } + + private String getBranchId(String revisionId) throws Exception { + if (revisionId == null) { + return null; + } + + CommitMongo baseCommit = new FetchCommitAction(mongoConnection, + MongoUtil.toMongoRepresentation(revisionId)).execute(); + return baseCommit.getBranchId(); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/BaseCommand.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/BaseCommand.java new file mode 100644 index 00000000000..99c1af656da --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/BaseCommand.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.mongomk.impl.command; + +import org.apache.jackrabbit.mongomk.api.command.Command; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; + +/** + * Base {@code Command} implementation. + * + * @param The result type of the {@code Command}. + */ +public abstract class BaseCommand implements Command { + + protected final MongoConnection mongoConnection; + + /** + * Constructs a default command with the supplied connection. + * + * @param mongoConnection The mongo connection. + */ + public BaseCommand(MongoConnection mongoConnection) { + this.mongoConnection = mongoConnection; + } + + @Override + public int getNumOfRetries() { + return 0; + } + + @Override + public boolean needsRetry(Exception e) { + return false; + } + + @Override + public boolean needsRetry(T result) { + return false; + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/CommitCommand.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/CommitCommand.java new file mode 100644 index 00000000000..4d60089b3d5 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/CommitCommand.java @@ -0,0 +1,313 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl.command; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.jackrabbit.mongomk.action.FetchCommitAction; +import org.apache.jackrabbit.mongomk.action.FetchNodesAction; +import org.apache.jackrabbit.mongomk.action.ReadAndIncHeadRevisionAction; +import org.apache.jackrabbit.mongomk.action.SaveAndSetHeadRevisionAction; +import org.apache.jackrabbit.mongomk.action.SaveCommitAction; +import org.apache.jackrabbit.mongomk.action.SaveNodesAction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction; +import org.apache.jackrabbit.mongomk.api.model.Commit; +import org.apache.jackrabbit.mongomk.command.exception.ConflictingCommitException; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.model.CommitCommandInstructionVisitor; +import org.apache.jackrabbit.mongomk.model.CommitMongo; +import org.apache.jackrabbit.mongomk.model.NodeMongo; +import org.apache.jackrabbit.mongomk.model.NotFoundException; +import org.apache.jackrabbit.mongomk.model.SyncMongo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBCollection; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; +import com.mongodb.WriteResult; + +/** + * {@code Command} for {@code MongoMicroKernel#commit(String, String, String, String)} + */ +public class CommitCommand extends BaseCommand { + + private static final Logger logger = LoggerFactory.getLogger(CommitCommand.class); + + private final Commit commit; + + private Set affectedPaths; + private CommitMongo commitMongo; + private List existingNodes; + private SyncMongo syncMongo; + private Set nodeMongos; + private Long revisionId; + private String branchId; + + /** + * Constructs a new {@code CommitCommandMongo}. + * + * @param mongoConnection {@link MongoConnection} + * @param commit {@link Commit} + */ + public CommitCommand(MongoConnection mongoConnection, Commit commit) { + super(mongoConnection); + this.commit = commit; + } + + @Override + public Long execute() throws Exception { + logger.debug(String.format("Trying to commit: %s", commit.getDiff())); + + readAndIncHeadRevision(); + createRevision(); + readBranchIdFromBaseCommit(); + createMongoNodes(); + createMongoCommit(); + readExistingNodes(); + mergeNodes(); + prepareMongoNodes(); + saveNodes(); + saveCommit(); + boolean success = saveAndSetHeadRevision(); + + logger.debug(String.format("Success was: %b", success)); + + if (!success) { + markAsFailed(); + throw new ConflictingCommitException(); + } + + addRevisionId(); + + return revisionId; + } + + @Override + public int getNumOfRetries() { + return 20; + } + + @Override + public boolean needsRetry(Exception e) { + // In createMongoNodes step, sometimes add operations could end up with + // not found exceptions in high concurrency situations. + return e instanceof ConflictingCommitException || e instanceof NotFoundException; + } + + /** + * FIXME - Currently this assumes a conflict if there's an update but it + * should really check the affected paths before assuming a conflict. When + * this is fixed, lower the number of retries. + * + * This is protected for testing purposed only. + * + * @return True if the operation was successful. + * @throws Exception If an exception happens. + */ + protected boolean saveAndSetHeadRevision() throws Exception { + SyncMongo syncMongo = new SaveAndSetHeadRevisionAction(mongoConnection, + this.syncMongo.getHeadRevisionId(), revisionId).execute(); + if (syncMongo == null) { + logger.warn(String.format("Encounterd a conflicting update, thus can't commit" + + " revision %s and will be retried with new revision", revisionId)); + return false; + } + return true; + } + + private void addRevisionId() { + commit.setRevisionId(revisionId); + } + + private void createMongoCommit() throws Exception { + commitMongo = CommitMongo.fromCommit(commit); + commitMongo.setRevisionId(revisionId); + commitMongo.setAffectedPaths(new LinkedList(affectedPaths)); + commitMongo.setBaseRevId(syncMongo.getHeadRevisionId()); + if (commitMongo.getBranchId() == null && branchId != null) { + commitMongo.setBranchId(branchId); + } + } + + private void createMongoNodes() throws Exception { + CommitCommandInstructionVisitor visitor = new CommitCommandInstructionVisitor( + mongoConnection, syncMongo.getHeadRevisionId()); + visitor.setBranchId(branchId); + + for (Instruction instruction : commit.getInstructions()) { + instruction.accept(visitor); + } + + Map pathNodeMap = visitor.getPathNodeMap(); + + affectedPaths = pathNodeMap.keySet(); + nodeMongos = new HashSet(pathNodeMap.values()); + for (NodeMongo nodeMongo : nodeMongos) { + nodeMongo.setRevisionId(revisionId); + if (branchId != null) { + nodeMongo.setBranchId(branchId); + } + } + } + + private void createRevision() { + revisionId = syncMongo.getNextRevisionId() - 1; + } + + private void markAsFailed() throws Exception { + DBCollection commitCollection = mongoConnection.getCommitCollection(); + DBObject query = QueryBuilder.start("_id").is(commitMongo.getObjectId("_id")).get(); + DBObject update = new BasicDBObject("$set", new BasicDBObject(CommitMongo.KEY_FAILED, Boolean.TRUE)); + WriteResult writeResult = commitCollection.update(query, update); + if (writeResult.getError() != null) { + // FIXME This is potentially a bug that we need to handle. + throw new Exception(String.format("Update wasn't successful: %s", writeResult)); + } + } + + private void mergeNodes() { + for (NodeMongo existingNode : existingNodes) { + for (NodeMongo committingNode : nodeMongos) { + if (existingNode.getPath().equals(committingNode.getPath())) { + logger.debug(String.format("Found existing node to merge: %s", existingNode.getPath())); + logger.debug(String.format("Existing node: %s", existingNode)); + logger.debug(String.format("Committing node: %s", committingNode)); + + Map existingProperties = existingNode.getProperties(); + if (!existingProperties.isEmpty()) { + committingNode.setProperties(existingProperties); + + logger.debug(String.format("Merged properties for %s: %s", existingNode.getPath(), + existingProperties)); + } + + List existingChildren = existingNode.getChildren(); + if (existingChildren != null) { + committingNode.setChildren(existingChildren); + + logger.debug(String.format("Merged children for %s: %s", existingNode.getPath(), existingChildren)); + } + + committingNode.setBaseRevisionId(existingNode.getRevisionId()); + + logger.debug(String.format("Merged node for %s: %s", existingNode.getPath(), committingNode)); + + break; + } + } + } + } + + private void prepareMongoNodes() { + for (NodeMongo committingNode : nodeMongos) { + logger.debug(String.format("Preparing children (added and removed) of %s", committingNode.getPath())); + logger.debug(String.format("Committing node: %s", committingNode)); + + List children = committingNode.getChildren(); + if (children == null) { + children = new LinkedList(); + } + + List addedChildren = committingNode.getAddedChildren(); + if (addedChildren != null) { + children.addAll(addedChildren); + } + + List removedChildren = committingNode.getRemovedChildren(); + if (removedChildren != null) { + children.removeAll(removedChildren); + } + + if (!children.isEmpty()) { + Set temp = new HashSet(children); // remove all duplicates + committingNode.setChildren(new LinkedList(temp)); + } else { + committingNode.setChildren(null); + } + + Map properties = committingNode.getProperties(); + + Map addedProperties = committingNode.getAddedProps(); + if (addedProperties != null) { + properties.putAll(addedProperties); + } + + Map removedProperties = committingNode.getRemovedProps(); + if (removedProperties != null) { + for (Map.Entry entry : removedProperties.entrySet()) { + properties.remove(entry.getKey()); + } + } + + if (!properties.isEmpty()) { + committingNode.setProperties(properties); + } else { + committingNode.setProperties(null); + } + + logger.debug(String.format("Prepared committing node: %s", committingNode)); + } + } + + private void readBranchIdFromBaseCommit() throws Exception { + String commitBranchId = commit.getBranchId(); + if (commitBranchId != null) { + // This is a newly created branch, so no need to check the base + // commit's branch id. + branchId = commitBranchId; + return; + } + + Long baseRevisionId = commit.getBaseRevisionId(); + if (baseRevisionId == null) { + return; + } + + CommitMongo baseCommit = new FetchCommitAction(mongoConnection, baseRevisionId).execute(); + branchId = baseCommit.getBranchId(); + } + + private void readAndIncHeadRevision() throws Exception { + syncMongo = new ReadAndIncHeadRevisionAction(mongoConnection).execute(); + } + + private void readExistingNodes() { + Set paths = new HashSet(); + for (NodeMongo nodeMongo : nodeMongos) { + paths.add(nodeMongo.getPath()); + } + + FetchNodesAction action = new FetchNodesAction(mongoConnection, paths, + syncMongo.getHeadRevisionId()); + action.setBranchId(branchId); + existingNodes = action.execute(); + } + + private void saveCommit() throws Exception { + new SaveCommitAction(mongoConnection, commitMongo).execute(); + } + + private void saveNodes() throws Exception { + new SaveNodesAction(mongoConnection, nodeMongos).execute(); + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/DefaultCommandExecutor.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/DefaultCommandExecutor.java new file mode 100644 index 00000000000..8fd4d608604 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/DefaultCommandExecutor.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.mongomk.impl.command; + +import org.apache.jackrabbit.mongomk.api.command.Command; +import org.apache.jackrabbit.mongomk.api.command.CommandExecutor; + +/** + * Implementation of the {@link CommandExecutor} interface. + */ +public class DefaultCommandExecutor implements CommandExecutor { + + @Override + public T execute(Command command) throws Exception { + T result = null; + + int numOfRetries = command.getNumOfRetries(); + int currentRetry = 0; + boolean needsRetry = true; + + while ((currentRetry <= numOfRetries) && needsRetry) { + + try { + result = command.execute(); + needsRetry = command.needsRetry(result); + } catch (Exception e) { + needsRetry = command.needsRetry(e); + + if (!needsRetry || currentRetry >= numOfRetries) { + throw e; + } + } + + ++currentRetry; + } + + return result; + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/DiffCommand.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/DiffCommand.java new file mode 100644 index 00000000000..b682cdd2ef5 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/DiffCommand.java @@ -0,0 +1,90 @@ +package org.apache.jackrabbit.mongomk.impl.command; + +import org.apache.jackrabbit.mk.model.tree.DiffBuilder; +import org.apache.jackrabbit.mk.model.tree.NodeState; +import org.apache.jackrabbit.mongomk.action.FetchCommitAction; +import org.apache.jackrabbit.mongomk.action.FetchHeadRevisionIdAction; +import org.apache.jackrabbit.mongomk.api.model.Node; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.impl.model.tree.MongoNodeStore; +import org.apache.jackrabbit.mongomk.model.CommitMongo; +import org.apache.jackrabbit.mongomk.util.MongoUtil; + +/** + * A {@code Command} for {@code MongoMicroKernel#diff(String, String, String, int)} + */ +public class DiffCommand extends BaseCommand { + + private final String fromRevision; + private final String toRevision; + private final int depth; + + private String path; + + /** + * Constructs a {@code DiffCommandCommandMongo} + * + * @param mongoConnection Mongo connection + * @param fromRevision From revision id. + * @param toRevision To revision id. + * @param path Path. + * @param depth Depth. + */ + public DiffCommand(MongoConnection mongoConnection, String fromRevision, + String toRevision, String path, int depth) { + super(mongoConnection); + this.fromRevision = fromRevision; + this.toRevision = toRevision; + this.path = path; + this.depth = depth; + } + + @Override + public String execute() throws Exception { + path = MongoUtil.adjustPath(path); + checkDepth(); + + long fromRevisionId, toRevisionId; + if (fromRevision == null || toRevision == null) { + FetchHeadRevisionIdAction query = new FetchHeadRevisionIdAction(mongoConnection); + query.includeBranchCommits(true); + long head = query.execute(); + fromRevisionId = fromRevision == null? head : MongoUtil.toMongoRepresentation(fromRevision); + toRevisionId = toRevision == null ? head : MongoUtil.toMongoRepresentation(toRevision);; + } else { + fromRevisionId = MongoUtil.toMongoRepresentation(fromRevision); + toRevisionId = MongoUtil.toMongoRepresentation(toRevision);; + } + + if (fromRevisionId == toRevisionId) { + return ""; + } + + if ("/".equals(path)) { + CommitMongo toCommit = new FetchCommitAction(mongoConnection, toRevisionId).execute(); + if (toCommit.getBaseRevId() == fromRevisionId) { + // Specified range spans a single commit: + // use diff stored in commit instead of building it dynamically + return toCommit.getDiff(); + } + } + + NodeState beforeState = MongoUtil.wrap(getNode(path, fromRevisionId)); + NodeState afterState = MongoUtil.wrap(getNode(path, toRevisionId)); + + return new DiffBuilder(beforeState, afterState, path, depth, + new MongoNodeStore(), path).build(); + } + + private void checkDepth() { + if (depth < -1) { + throw new IllegalArgumentException("depth"); + } + } + + private Node getNode(String path, long revisionId) throws Exception { + GetNodesCommand command = new GetNodesCommand(mongoConnection, + path, revisionId); + return command.execute(); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/GetBlobLengthCommand.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/GetBlobLengthCommand.java new file mode 100644 index 00000000000..c3e02f04b8d --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/GetBlobLengthCommand.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.mongomk.impl.command; + +import org.apache.jackrabbit.mongomk.impl.MongoConnection; + +import com.mongodb.BasicDBObject; +import com.mongodb.gridfs.GridFS; +import com.mongodb.gridfs.GridFSDBFile; + +/** + * {@code Command} for {@code MongoMicroKernel#getLength(String)} + */ +public class GetBlobLengthCommand extends BaseCommand { + + private final String blobId; + + /** + * Constructs the command. + * + * @param mongoConnection Mongo connection. + * @param blobId Blob id. + */ + public GetBlobLengthCommand(MongoConnection mongoConnection, String blobId) { + super(mongoConnection); + this.blobId = blobId; + } + + @Override + public Long execute() throws Exception { + GridFS gridFS = mongoConnection.getGridFS(); + GridFSDBFile gridFSDBFile = gridFS.findOne(new BasicDBObject("md5", blobId)); + if (gridFSDBFile == null) { + throw new Exception("Blob does not exist"); + } + return gridFSDBFile.getLength(); + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/GetHeadRevisionCommand.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/GetHeadRevisionCommand.java new file mode 100644 index 00000000000..3785f0e76d6 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/GetHeadRevisionCommand.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.mongomk.impl.command; + +import org.apache.jackrabbit.mongomk.action.FetchHeadRevisionIdAction; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; + +/** + * {@code Command} for {@code MongoMicroKernel#getHeadRevision()} + */ +public class GetHeadRevisionCommand extends BaseCommand { + + /** + * Constructs a new {@code GetHeadRevisionCommandMongo}. + * + * @param mongoConnection The {@link MongoConnection}. + */ + public GetHeadRevisionCommand(MongoConnection mongoConnection) { + super(mongoConnection); + } + + @Override + public Long execute() throws Exception { + return new FetchHeadRevisionIdAction(mongoConnection).execute(); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/GetJournalCommand.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/GetJournalCommand.java new file mode 100644 index 00000000000..0d019674b90 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/GetJournalCommand.java @@ -0,0 +1,125 @@ +package org.apache.jackrabbit.mongomk.impl.command; + +import java.util.List; + +import org.apache.jackrabbit.mk.api.MicroKernelException; +import org.apache.jackrabbit.mk.json.JsopBuilder; +import org.apache.jackrabbit.mk.model.tree.DiffBuilder; +import org.apache.jackrabbit.mongomk.action.FetchCommitsAction; +import org.apache.jackrabbit.mongomk.action.FetchHeadRevisionIdAction; +import org.apache.jackrabbit.mongomk.api.model.Node; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.impl.model.tree.MongoNodeStore; +import org.apache.jackrabbit.mongomk.model.CommitMongo; +import org.apache.jackrabbit.mongomk.util.MongoUtil; + +/** + * A {@code Command} for {@code MongoMicroKernel#getJournal(String, String, String)} + */ +public class GetJournalCommand extends BaseCommand { + + private final String fromRevisionId; + private final String toRevisionId; + + private String path; + + /** + * Constructs a {@code GetJournalCommandMongo} + * + * @param mongoConnection Mongo connection. + * @param fromRevisionId From revision. + * @param toRevisionId To revision. + * @param path Path. + */ + public GetJournalCommand(MongoConnection mongoConnection, String fromRevisionId, + String toRevisionId, String path) { + super(mongoConnection); + this.fromRevisionId = fromRevisionId; + this.toRevisionId = toRevisionId; + this.path = path; + } + + @Override + public String execute() throws Exception { + path = MongoUtil.adjustPath(path); + + long fromRevision = MongoUtil.toMongoRepresentation(fromRevisionId); + long toRevision; + if (toRevisionId == null) { + FetchHeadRevisionIdAction query = new FetchHeadRevisionIdAction(mongoConnection); + query.includeBranchCommits(true); + toRevision = query.execute(); + } else { + toRevision = MongoUtil.toMongoRepresentation(toRevisionId); + } + + List commits = getCommits(fromRevision, toRevision); + + CommitMongo toCommit = extractCommit(commits, toRevision); + if (toCommit.getBranchId() != null) { + throw new MicroKernelException("Branch revisions are not supported: " + toRevisionId); + } + + CommitMongo fromCommit; + if (toRevision == fromRevision) { + fromCommit = toCommit; + } else { + fromCommit = extractCommit(commits, fromRevision); + if (fromCommit == null || (fromCommit.getTimestamp() > toCommit.getTimestamp())) { + // negative range, return empty journal + return "[]"; + } + } + if (fromCommit.getBranchId() != null) { + throw new MicroKernelException("Branch revisions are not supported: " + fromRevisionId); + } + + JsopBuilder commitBuff = new JsopBuilder().array(); + // Iterate over commits in chronological order, starting with oldest commit + for (int i = commits.size() - 1; i >= 0; i--) { + CommitMongo commit = commits.get(i); + + String diff = commit.getDiff(); + if (MongoUtil.isFiltered(path)) { + try { + diff = new DiffBuilder( + MongoUtil.wrap(getNode("/", commit.getBaseRevId())), + MongoUtil.wrap(getNode("/", commit.getRevisionId())), + "/", -1, new MongoNodeStore(), path).build(); + if (diff.isEmpty()) { + continue; + } + } catch (Exception e) { + throw new MicroKernelException(e); + } + } + commitBuff.object() + .key("id").value(MongoUtil.fromMongoRepresentation(commit.getRevisionId())) + .key("ts").value(commit.getTimestamp()) + .key("msg").value(commit.getMessage()) + .key("changes").value(diff).endObject(); + } + return commitBuff.endArray().toString(); + } + + private CommitMongo extractCommit(List commits, long revisionId) { + for (CommitMongo commit : commits) { + if (commit.getRevisionId() == revisionId) { + return commit; + } + } + return null; + } + + private List getCommits(long fromRevisionId, long toRevisionId) { + FetchCommitsAction query = new FetchCommitsAction(mongoConnection, fromRevisionId, + toRevisionId); + return query.execute(); + } + + private Node getNode(String path, long revisionId) throws Exception { + GetNodesCommand command = new GetNodesCommand(mongoConnection, + path, revisionId); + return command.execute(); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/GetNodesCommand.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/GetNodesCommand.java new file mode 100644 index 00000000000..de15aebd4b0 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/GetNodesCommand.java @@ -0,0 +1,248 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl.command; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; + +import org.apache.jackrabbit.mongomk.action.FetchCommitAction; +import org.apache.jackrabbit.mongomk.action.FetchCommitsAction; +import org.apache.jackrabbit.mongomk.action.FetchNodesAction; +import org.apache.jackrabbit.mongomk.api.model.Node; +import org.apache.jackrabbit.mongomk.command.exception.InconsistentNodeHierarchyException; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.impl.model.NodeImpl; +import org.apache.jackrabbit.mongomk.model.CommitMongo; +import org.apache.jackrabbit.mongomk.model.NodeMongo; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@code Command} for {@code MongoMicroKernel#getNodes(String, String, int, long, int, String)} + */ +public class GetNodesCommand extends BaseCommand { + + private static final Logger LOG = LoggerFactory.getLogger(GetNodesCommand.class); + + private final String path; + + private String branchId; + private int depth = FetchNodesAction.LIMITLESS_DEPTH; + private Long revisionId; + private List lastCommits; + private List nodeMongos; + + private Map pathAndNodeMap; + private Map problematicNodes; + private Node rootNode; + + /** + * Constructs a new {@code GetNodesCommandMongo}. + * + * @param mongoConnection The {@link MongoConnection}. + * @param path The root path of the nodes to get. + * @param revisionId The revision id or null for head revision. + */ + public GetNodesCommand(MongoConnection mongoConnection, String path, + Long revisionId) { + super(mongoConnection); + this.path = path; + this.revisionId = revisionId; + } + + /** + * Sets the branchId for the command. + * + * @param branchId Branch id. + */ + public void setBranchId(String branchId) { + this.branchId = branchId; + } + + /** + * Sets the depth for the command. + * + * @param depth The depth for the command or -1 for limitless depth. + */ + public void setDepth(int depth) { + this.depth = depth; + } + + @Override + public Node execute() throws Exception { + ensureRevisionId(); + readLastCommits(); + deriveProblematicNodes(); + readRootNode(); + return rootNode; + } + + private void readRootNode() throws InconsistentNodeHierarchyException { + readNodesByPath(); + createPathAndNodeMap(); + boolean verified = verifyProblematicNodes() && verifyNodeHierarchy(); + if (!verified) { + throw new InconsistentNodeHierarchyException(); + } + buildNodeStructure(); + } + + @Override + public int getNumOfRetries() { + return 3; + } + + @Override + public boolean needsRetry(Exception e) { + return e instanceof InconsistentNodeHierarchyException; + } + + private void buildNodeStructure() { + NodeMongo nodeMongoRootOfPath = pathAndNodeMap.get(path); + rootNode = buildNodeStructure(nodeMongoRootOfPath); + } + + private NodeImpl buildNodeStructure(NodeMongo nodeMongo) { + if (nodeMongo == null) { + return null; + } + + NodeImpl node = NodeMongo.toNode(nodeMongo); + + for (Iterator it = node.getChildNodeEntries(0, -1); it.hasNext(); ) { + Node child = it.next(); + NodeMongo nodeMongoChild = pathAndNodeMap.get(child.getPath()); + if (nodeMongoChild != null) { + NodeImpl nodeChild = buildNodeStructure(nodeMongoChild); + node.addChildNodeEntry(nodeChild); + } + } + + return node; + } + + private void createPathAndNodeMap() { + pathAndNodeMap = new HashMap(); + for (NodeMongo nodeMongo : nodeMongos) { + pathAndNodeMap.put(nodeMongo.getPath(), nodeMongo); + } + } + + private void deriveProblematicNodes() { + problematicNodes = new HashMap(); + + for (ListIterator iterator = lastCommits.listIterator(); iterator.hasPrevious();) { + CommitMongo commitMongo = iterator.previous(); + long revisionId = commitMongo.getRevisionId(); + List affectedPath = commitMongo.getAffectedPaths(); + + for (String path : affectedPath) { + problematicNodes.put(path, revisionId); + } + } + } + + private void ensureRevisionId() throws Exception { + if (revisionId == null) { + revisionId = new GetHeadRevisionCommand(mongoConnection).execute(); + } else { + // Ensure that commit with revision id exists. + new FetchCommitAction(mongoConnection, revisionId).execute(); + } + } + + private void readLastCommits() throws Exception { + lastCommits = new FetchCommitsAction(mongoConnection, revisionId).execute(); + } + + private void readNodesByPath() { + FetchNodesAction query = new FetchNodesAction(mongoConnection, + path, true, revisionId); + query.setBranchId(branchId); + // FIXME - This does not work for depth > 3449. + //query.setDepth(depth); + nodeMongos = query.execute(); + } + + private boolean verifyNodeHierarchy() { + boolean verified = false; + + verified = verifyNodeHierarchyRec(path, 0); + + if (!verified) { + LOG.error(String.format("Node hierarchy could not be verified because" + + " some nodes were inconsistent: %s", path)); + } + + return verified; + } + + private boolean verifyNodeHierarchyRec(String path, int currentDepth) { + boolean verified = false; + + if (pathAndNodeMap.isEmpty()) { + return true; + } + + NodeMongo nodeMongo = pathAndNodeMap.get(path); + if (nodeMongo != null) { + verified = true; + if ((depth == -1) || (currentDepth < depth)) { + List childNames = nodeMongo.getChildren(); + if (childNames != null) { + for (String childName : childNames) { + String childPath = PathUtils.concat(path, childName); + verified = verifyNodeHierarchyRec(childPath, ++currentDepth); + if (!verified) { + break; + } + } + } + } + } + + return verified; + } + + private boolean verifyProblematicNodes() { + boolean verified = true; + + for (Map.Entry entry : problematicNodes.entrySet()) { + String path = entry.getKey(); + Long revisionId = entry.getValue(); + + NodeMongo nodeMongo = pathAndNodeMap.get(path); + if (nodeMongo != null) { + if (!revisionId.equals(nodeMongo.getRevisionId())) { + verified = false; + + LOG.error(String + .format("Node could not be verified because the expected revisionId did not match: %d (expected) vs %d (actual)", + revisionId, nodeMongo.getRevisionId())); + + break; + } + } + } + + return verified; + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/GetRevisionHistoryCommand.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/GetRevisionHistoryCommand.java new file mode 100644 index 00000000000..14b35ae44ee --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/GetRevisionHistoryCommand.java @@ -0,0 +1,91 @@ +package org.apache.jackrabbit.mongomk.impl.command; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.jackrabbit.mk.api.MicroKernelException; +import org.apache.jackrabbit.mk.json.JsopBuilder; +import org.apache.jackrabbit.mk.model.tree.DiffBuilder; +import org.apache.jackrabbit.mongomk.action.FetchCommitsAction; +import org.apache.jackrabbit.mongomk.api.model.Node; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.impl.model.tree.MongoNodeStore; +import org.apache.jackrabbit.mongomk.model.CommitMongo; +import org.apache.jackrabbit.mongomk.util.MongoUtil; + +/** + * A {@code Command} for {@code MongoMicroKernel#getRevisionHistory(long, int, String)} + */ +public class GetRevisionHistoryCommand extends BaseCommand { + + private final long since; + + private int maxEntries; + private String path; + + /** + * Constructs a {@code GetRevisionHistoryCommandMongo} + * + * @param mongoConnection Mongo connection. + * @param since Timestamp (ms) of earliest revision to be returned + * @param maxEntries maximum #entries to be returned; if < 0, no limit will be applied. + * @param path optional path filter; if {@code null} or {@code ""} the + * default ({@code "/"}) will be assumed, i.e. no filter will be applied + */ + public GetRevisionHistoryCommand(MongoConnection mongoConnection, + long since, int maxEntries, String path) { + super(mongoConnection); + this.since = since; + this.maxEntries = maxEntries; + this.path = path; + } + + @Override + public String execute() { + path = MongoUtil.adjustPath(path); + maxEntries = maxEntries < 0 ? Integer.MAX_VALUE : maxEntries; + + FetchCommitsAction action = new FetchCommitsAction(mongoConnection); + action.setMaxEntries(maxEntries); + action.includeBranchCommits(false); + + List commits = action.execute(); + List history = new ArrayList(); + for (int i = commits.size() - 1; i >= 0; i--) { + CommitMongo commit = commits.get(i); + if (commit.getTimestamp() >= since) { + if (MongoUtil.isFiltered(path)) { + try { + String diff = new DiffBuilder( + MongoUtil.wrap(getNode("/", commit.getBaseRevId())), + MongoUtil.wrap(getNode("/", commit.getRevisionId())), + "/", -1, new MongoNodeStore(), path).build(); + if (!diff.isEmpty()) { + history.add(commit); + } + } catch (Exception e) { + throw new MicroKernelException(e); + } + } else { + history.add(commit); + } + } + } + + JsopBuilder buff = new JsopBuilder().array(); + for (CommitMongo commit : history) { + buff.object() + .key("id").value(MongoUtil.fromMongoRepresentation(commit.getRevisionId())) + .key("ts").value(commit.getTimestamp()) + .key("msg").value(commit.getMessage()) + .endObject(); + } + return buff.endArray().toString(); + } + + private Node getNode(String path, long revisionId) throws Exception { + GetNodesCommand command = new GetNodesCommand(mongoConnection, + path, revisionId); + return command.execute(); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/MergeCommand.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/MergeCommand.java new file mode 100644 index 00000000000..68f0e6eafc5 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/MergeCommand.java @@ -0,0 +1,189 @@ +package org.apache.jackrabbit.mongomk.impl.command; + +import java.util.List; +import java.util.Map; + +import org.apache.jackrabbit.mk.model.tree.DiffBuilder; +import org.apache.jackrabbit.mk.model.tree.NodeState; +import org.apache.jackrabbit.mongomk.action.FetchBranchBaseRevisionIdAction; +import org.apache.jackrabbit.mongomk.action.FetchCommitAction; +import org.apache.jackrabbit.mongomk.action.FetchHeadRevisionIdAction; +import org.apache.jackrabbit.mongomk.api.command.Command; +import org.apache.jackrabbit.mongomk.api.model.Commit; +import org.apache.jackrabbit.mongomk.api.model.Node; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.impl.model.CommitBuilder; +import org.apache.jackrabbit.mongomk.impl.model.NodeImpl; +import org.apache.jackrabbit.mongomk.impl.model.tree.MongoNodeDelta; +import org.apache.jackrabbit.mongomk.impl.model.tree.MongoNodeDelta.Conflict; +import org.apache.jackrabbit.mongomk.impl.model.tree.MongoNodeState; +import org.apache.jackrabbit.mongomk.impl.model.tree.MongoNodeStore; +import org.apache.jackrabbit.mongomk.model.CommitMongo; +import org.apache.jackrabbit.mongomk.util.MongoUtil; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@code Command} for {@code MongoMicroKernel#merge(String, String)} + */ +public class MergeCommand extends BaseCommand { + + private static final Logger LOG = LoggerFactory.getLogger(MergeCommand.class); + + private final String branchRevisionId; + private final String message; + + /** + * Constructs a {@code MergeCommandMongo} + * + * @param mongoConnection Mongo connection. + * @param branchRevisionId Branch revision id. + * @param message Merge message. + */ + public MergeCommand(MongoConnection mongoConnection, String branchRevisionId, + String message) { + super(mongoConnection); + this.branchRevisionId = branchRevisionId; + this.message = message; + } + + @Override + public String execute() throws Exception { + CommitMongo commit = new FetchCommitAction(mongoConnection, + MongoUtil.toMongoRepresentation(branchRevisionId)).execute(); + String branchId = commit.getBranchId(); + if (branchId == null) { + throw new Exception("Can only merge a private branch commit"); + } + + long rootNodeId = commit.getRevisionId(); + + FetchHeadRevisionIdAction query2 = new FetchHeadRevisionIdAction(mongoConnection); + query2.includeBranchCommits(false); + long currentHead = query2.execute(); + + Node ourRoot = getNode("/", rootNodeId, branchId); + + FetchBranchBaseRevisionIdAction branchAction = new FetchBranchBaseRevisionIdAction(mongoConnection, branchId); + long branchRootId = branchAction.execute(); + + // Merge nodes from head to branch. + ourRoot = mergeNodes(ourRoot, currentHead, branchRootId); + + Node currentHeadNode = getNode("/", currentHead); + + String diff = new DiffBuilder(MongoUtil.wrap(currentHeadNode), + MongoUtil.wrap(ourRoot), "/", -1, + new MongoNodeStore(), "").build(); + + if (diff.isEmpty()) { + LOG.debug("Merge of empty branch {} with differing content hashes encountered, " + + "ignore and keep current head {}", branchRevisionId, currentHead); + return MongoUtil.fromMongoRepresentation(currentHead); + } + + Commit newCommit = CommitBuilder.build("", diff, + MongoUtil.fromMongoRepresentation(currentHead), message); + + Command command = new CommitCommand(mongoConnection, newCommit); + long revision = command.execute(); + return MongoUtil.fromMongoRepresentation(revision); + } + + private NodeImpl mergeNodes(Node ourRoot, Long newBaseRevisionId, + Long commonAncestorRevisionId) throws Exception { + + Node baseRoot = getNode("/", commonAncestorRevisionId); + Node theirRoot = getNode("/", newBaseRevisionId); + + // Recursively merge 'our' changes with 'their' changes... + NodeImpl mergedNode = mergeNode(baseRoot, ourRoot, theirRoot, "/"); + + return mergedNode; + } + + private NodeImpl mergeNode(Node baseNode, Node ourNode, Node theirNode, + String path) throws Exception { + MongoNodeDelta theirChanges = new MongoNodeDelta(new MongoNodeStore(), + MongoUtil.wrap(baseNode), MongoUtil.wrap(theirNode)); + MongoNodeDelta ourChanges = new MongoNodeDelta(new MongoNodeStore(), + MongoUtil.wrap(baseNode), MongoUtil.wrap(ourNode)); + + NodeImpl stagedNode = (NodeImpl)theirNode; //new NodeImpl(path); + + // Apply our changes. + stagedNode.getProperties().putAll(ourChanges.getAddedProperties()); + stagedNode.getProperties().putAll(ourChanges.getChangedProperties()); + for (String name : ourChanges.getRemovedProperties().keySet()) { + stagedNode.getProperties().remove(name); + } + + for (Map.Entry entry : ourChanges.getAddedChildNodes().entrySet()) { + MongoNodeState nodeState = (MongoNodeState)entry.getValue(); + stagedNode.addChildNodeEntry(nodeState.unwrap()); + } + for (Map.Entry entry : ourChanges.getChangedChildNodes().entrySet()) { + if (!theirChanges.getChangedChildNodes().containsKey(entry.getKey())) { + MongoNodeState nodeState = (MongoNodeState)entry.getValue(); + stagedNode.addChildNodeEntry(nodeState.unwrap()); + } + } + for (String name : ourChanges.getRemovedChildNodes().keySet()) { + stagedNode.removeChildNodeEntry(name); + } + + List conflicts = theirChanges.listConflicts(ourChanges); + // resolve/report merge conflicts + for (Conflict conflict : conflicts) { + String conflictName = conflict.getName(); + String conflictPath = PathUtils.concat(path, conflictName); + switch (conflict.getType()) { + case PROPERTY_VALUE_CONFLICT: + throw new Exception( + "concurrent modification of property " + conflictPath + + " with conflicting values: \"" + + ourNode.getProperties().get(conflictName) + + "\", \"" + + theirNode.getProperties().get(conflictName)); + + case NODE_CONTENT_CONFLICT: { + if (ourChanges.getChangedChildNodes().containsKey(conflictName)) { + // modified subtrees + Node baseChild = baseNode.getChildNodeEntry(conflictName); + Node ourChild = ourNode.getChildNodeEntry(conflictName); + Node theirChild = theirNode.getChildNodeEntry(conflictName); + // merge the dirty subtrees recursively + mergeNode(baseChild, ourChild, theirChild, PathUtils.concat(path, conflictName)); + } else { + // todo handle/merge colliding node creation + throw new Exception("colliding concurrent node creation: " + conflictPath); + } + break; + } + + case REMOVED_DIRTY_PROPERTY_CONFLICT: + stagedNode.getProperties().remove(conflictName); + break; + + case REMOVED_DIRTY_NODE_CONFLICT: + //stagedNode.remove(conflictName); + stagedNode.removeChildNodeEntry(conflictName); + break; + } + + } + return stagedNode; + } + + private Node getNode(String path, long revisionId) throws Exception { + return getNode(path, revisionId, null); + } + + private Node getNode(String path, long revisionId, String branchId) throws Exception { + GetNodesCommand command = new GetNodesCommand(mongoConnection, + path, revisionId); + command.setBranchId(branchId); + return command.execute(); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/NodeExistsCommand.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/NodeExistsCommand.java new file mode 100644 index 00000000000..9de4f43b60a --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/NodeExistsCommand.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl.command; + +import org.apache.jackrabbit.mongomk.api.model.Node; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.oak.commons.PathUtils; + +/** + * {@code Command} for {@code MongoMicroKernel#nodeExists(String, String)} + */ +public class NodeExistsCommand extends BaseCommand { + + private final Long revisionId; + + private String branchId; + private Node parentNode; + private String path; + + /** + * Constructs a new {@code NodeExistsCommandMongo}. + * + * @param mongoConnection The {@link MongoConnection}. + * @param path The root path of the nodes to get. + * @param revisionId The revision id or null. + */ + public NodeExistsCommand(MongoConnection mongoConnection, String path, + Long revisionId) { + super(mongoConnection); + this.path = path; + this.revisionId = revisionId; + } + + /** + * Sets the branchId for the command. + * + * @param branchId Branch id. + */ + public void setBranchId(String branchId) { + this.branchId = branchId; + } + + @Override + public Boolean execute() throws Exception { + if (PathUtils.denotesRoot(path)) { + return true; + } + + // Check that all the paths up to the root actually exist. + return pathExists(); + } + + private boolean pathExists() throws Exception { + while (!PathUtils.denotesRoot(path)) { + readParentNode(revisionId, branchId); + if (parentNode == null || !childExists()) { + return false; + } + path = PathUtils.getParentPath(path); + } + + return true; + } + + private void readParentNode(Long revisionId, String branchId) throws Exception { + String parentPath = PathUtils.getParentPath(path); + GetNodesCommand command = new GetNodesCommand(mongoConnection, + parentPath, revisionId); + command.setBranchId(branchId); + parentNode = command.execute(); + } + + private boolean childExists() { + String childName = PathUtils.getName(path); + return parentNode.getChildNodeEntry(childName) != null; + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/ReadBlobCommand.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/ReadBlobCommand.java new file mode 100644 index 00000000000..d721f7bfe58 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/ReadBlobCommand.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.mongomk.impl.command; + +import java.io.InputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; + +import com.mongodb.BasicDBObject; +import com.mongodb.gridfs.GridFS; +import com.mongodb.gridfs.GridFSDBFile; + +/** + * {@code Command} for {@code MongoMicroKernel#read(String, long, byte[], int, int)} + * FIXME - Reading from large blobs with small increments is slow in this implementation. + * See if this could be improved with some kind of cache mechanism. + */ +public class ReadBlobCommand extends BaseCommand { + + private final String blobId; + private final long blobOffset; + private final byte[] buffer; + private final int bufferOffset; + private final int length; + + /** + * Constructs a new {@code ReadBlobCommandMongo}. + * + * @param mongoConnection Mongo connection. + * @param blobId Blob id. + * @param blobOffset Blob offset. + * @param buffer Buffer. + * @param bufferOffset Buffer offset. + * @param length Length. + */ + public ReadBlobCommand(MongoConnection mongoConnection, String blobId, + long blobOffset, byte[] buffer, int bufferOffset, int length) { + super(mongoConnection); + this.blobId = blobId; + this.blobOffset = blobOffset; + this.buffer = buffer; + this.bufferOffset = bufferOffset; + this.length = length; + } + + @Override + public Integer execute() throws Exception { + return fetchBlobFromMongo(); + } + + private int fetchBlobFromMongo() throws Exception { + GridFS gridFS = mongoConnection.getGridFS(); + GridFSDBFile gridFile = gridFS.findOne(new BasicDBObject("md5", blobId)); + long fileLength = gridFile.getLength(); + + long start = blobOffset; + long end = blobOffset + length; + if (end > fileLength) { + end = fileLength; + } + + int totalBytes = -1; + if (start < end) { + InputStream is = gridFile.getInputStream(); + IOUtils.skipFully(is, blobOffset); + totalBytes = is.read(buffer, bufferOffset, length); + is.close(); + } + return totalBytes; + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/WaitForCommitCommand.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/WaitForCommitCommand.java new file mode 100644 index 00000000000..329ee40f617 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/WaitForCommitCommand.java @@ -0,0 +1,61 @@ +package org.apache.jackrabbit.mongomk.impl.command; + +import org.apache.jackrabbit.mongomk.action.FetchHeadRevisionIdAction; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.util.MongoUtil; + +/** + * A {@code Command} for {@code MongoMicroKernel#waitForCommit(String, long)} + */ +public class WaitForCommitCommand extends BaseCommand { + + private static final long WAIT_FOR_COMMIT_POLL_MILLIS = 1000; + + private final String oldHeadRevisionId; + private final long timeout; + + /** + * Constructs a {@code WaitForCommitCommandMongo} + * + * @param mongoConnection Mongo connection. + * @param oldHeadRevisionId Id of earlier head revision + * @param timeout The maximum time to wait in milliseconds + */ + public WaitForCommitCommand(MongoConnection mongoConnection, String oldHeadRevisionId, + long timeout) { + super(mongoConnection); + this.oldHeadRevisionId = oldHeadRevisionId; + this.timeout = timeout; + } + + @Override + public Long execute() throws Exception { + long startTimestamp = System.currentTimeMillis(); + Long initialHeadRevisionId = getHeadRevision(); + + if (timeout <= 0) { + return initialHeadRevisionId; + } + + Long oldHeadRevision = MongoUtil.toMongoRepresentation(oldHeadRevisionId); + if (oldHeadRevision != initialHeadRevisionId) { + return initialHeadRevisionId; + } + + long waitForCommitPollMillis = Math.min(WAIT_FOR_COMMIT_POLL_MILLIS, timeout); + while (true) { + long headRevisionId = getHeadRevision(); + long now = System.currentTimeMillis(); + if (headRevisionId != initialHeadRevisionId || now - startTimestamp >= timeout) { + return headRevisionId; + } + Thread.sleep(waitForCommitPollMillis); + } + } + + private long getHeadRevision() throws Exception { + FetchHeadRevisionIdAction query = new FetchHeadRevisionIdAction(mongoConnection); + query.includeBranchCommits(false); + return query.execute(); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/WriteBlobCommand.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/WriteBlobCommand.java new file mode 100644 index 00000000000..e742b4a0e60 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/command/WriteBlobCommand.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl.command; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; + +import com.mongodb.BasicDBObject; +import com.mongodb.gridfs.GridFS; +import com.mongodb.gridfs.GridFSDBFile; +import com.mongodb.gridfs.GridFSInputFile; + +/** + * {@code Command} for {@code MongoMicroKernel#write(InputStream)} + */ +public class WriteBlobCommand extends BaseCommand { + + private final InputStream is; + + /** + * Constructs a {@code WriteBlobCommandMongo} + * + * @param mongoConnection Mongo connection. + * @param is Input stream. + */ + public WriteBlobCommand(MongoConnection mongoConnection, InputStream is) { + super(mongoConnection); + this.is = is; + } + + @Override + public String execute() throws Exception { + return saveBlob(); + } + + private String saveBlob() throws IOException { + GridFS gridFS = mongoConnection.getGridFS(); + BufferedInputStream bis = new BufferedInputStream(is); + String md5 = calculateMd5(bis); + GridFSDBFile gridFile = gridFS.findOne(new BasicDBObject("md5", md5)); + if (gridFile != null) { + is.close(); + return md5; + } + + GridFSInputFile gridFSInputFile = gridFS.createFile(bis, true); + gridFSInputFile.save(); + return gridFSInputFile.getMD5(); + } + + private String calculateMd5(BufferedInputStream bis) throws IOException { + bis.mark(Integer.MAX_VALUE); + String md5 = DigestUtils.md5Hex(bis); + bis.reset(); + return md5; + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/instruction/AddNodeInstructionImpl.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/instruction/AddNodeInstructionImpl.java new file mode 100644 index 00000000000..252815c3481 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/instruction/AddNodeInstructionImpl.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.mongomk.impl.instruction; + +import org.apache.jackrabbit.mongomk.api.instruction.InstructionVisitor; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.AddNodeInstruction; +import org.apache.jackrabbit.oak.commons.PathUtils; + +/** + * Implementation of the add node operation => "+" STRING ":" (OBJECT). + */ +public class AddNodeInstructionImpl extends BaseInstruction implements AddNodeInstruction { + + /** + * Constructs a new {@code AddNodeInstruction}. + * + * @param parentPath The parent path. + * @param name The name. + */ + public AddNodeInstructionImpl(String parentPath, String name) { + super(PathUtils.concat(parentPath, name)); + } + + @Override + public void accept(InstructionVisitor visitor) { + visitor.visit(this); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/instruction/BaseInstruction.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/instruction/BaseInstruction.java new file mode 100644 index 00000000000..97cd5123aa7 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/instruction/BaseInstruction.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl.instruction; + +import org.apache.jackrabbit.mongomk.api.instruction.Instruction; + +/** + * Base instruction implementation. + */ +public abstract class BaseInstruction implements Instruction { + + protected final String path; + + public BaseInstruction(String path) { + this.path = path; + } + + @Override + public String getPath() { + return path; + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/instruction/CopyNodeInstructionImpl.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/instruction/CopyNodeInstructionImpl.java new file mode 100644 index 00000000000..527270d4a8d --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/instruction/CopyNodeInstructionImpl.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.mongomk.impl.instruction; + +import org.apache.jackrabbit.mongomk.api.instruction.InstructionVisitor; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.CopyNodeInstruction; + +/** + * Implementation of the copy node operation => "*" STRING ":" STRING + */ +public class CopyNodeInstructionImpl extends BaseInstruction implements CopyNodeInstruction { + + private final String destPath; + private final String sourcePath; + + /** + * Constructs a new {@code CopyNodeInstruction}. + * + * @param path The path. + * @param sourcePath The source path. + * @param destPath The destination path. + */ + public CopyNodeInstructionImpl(String path, String sourcePath, String destPath) { + super(path); + this.sourcePath = sourcePath; + this.destPath = destPath; + } + + @Override + public void accept(InstructionVisitor visitor) { + visitor.visit(this); + } + + /** + * Returns the destination path. + * + * @return The destination path. + */ + public String getDestPath() { + return destPath; + } + + /** + * Returns the source path. + * + * @return The source path. + */ + public String getSourcePath() { + return sourcePath; + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/instruction/MoveNodeInstructionImpl.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/instruction/MoveNodeInstructionImpl.java new file mode 100644 index 00000000000..02c1daf5cfd --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/instruction/MoveNodeInstructionImpl.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.mongomk.impl.instruction; + +import org.apache.jackrabbit.mongomk.api.instruction.InstructionVisitor; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.MoveNodeInstruction; + +/** + * Implementation of the move node operation => ">" STRING ":" STRING + */ +public class MoveNodeInstructionImpl extends BaseInstruction implements MoveNodeInstruction { + + private final String destPath; + private final String sourcePath; + + /** + * Constructs a new {@code MoveNodeInstruction}. + * + * @param path The path. + * @param sourcePath The source path. + * @param destPath The destination path. + */ + public MoveNodeInstructionImpl(String path, String sourcePath, String destPath) { + super(path); + this.sourcePath = sourcePath; + this.destPath = destPath; + } + + @Override + public void accept(InstructionVisitor visitor) { + visitor.visit(this); + } + + /** + * Returns the destination path. + * + * @return The destination path. + */ + public String getDestPath() { + return destPath; + } + + /** + * Returns the source path. + * + * @return The source path. + */ + public String getSourcePath() { + return sourcePath; + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/instruction/RemoveNodeInstructionImpl.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/instruction/RemoveNodeInstructionImpl.java new file mode 100644 index 00000000000..ddc8dfd4601 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/instruction/RemoveNodeInstructionImpl.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.mongomk.impl.instruction; + +import org.apache.jackrabbit.mongomk.api.instruction.InstructionVisitor; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.RemoveNodeInstruction; +import org.apache.jackrabbit.oak.commons.PathUtils; + +/** + * Implementation for the remove node operation => "-" STRING + */ +public class RemoveNodeInstructionImpl extends BaseInstruction implements RemoveNodeInstruction { + + /** + * Constructs a new {@code RemoveNodeInstruction}. + * + * @param parentPath The parent path. + * @param name The name + */ + public RemoveNodeInstructionImpl(String parentPath, String name) { + super(PathUtils.concat(parentPath, name)); + } + + @Override + public void accept(InstructionVisitor visitor) { + visitor.visit(this); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/instruction/SetPropertyInstructionImpl.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/instruction/SetPropertyInstructionImpl.java new file mode 100644 index 00000000000..89b5d6675c3 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/instruction/SetPropertyInstructionImpl.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.mongomk.impl.instruction; + +import org.apache.jackrabbit.mongomk.api.instruction.InstructionVisitor; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.SetPropertyInstruction; + +/** + * Implementation for the set property operation => "^" STRING ":" ATOM | ARRAY + */ +public class SetPropertyInstructionImpl extends BaseInstruction implements SetPropertyInstruction { + + private final String key; + private final Object value; + + /** + * Constructs a new {@code SetPropertyInstruction}. + * + * @param path The path. + * @param key The key. + * @param value The value. + */ + public SetPropertyInstructionImpl(String path, String key, Object value) { + super(path); + this.key = key; + this.value = value; + } + + @Override + public void accept(InstructionVisitor visitor) { + visitor.visit(this); + } + + /** + * Returns the name of the property. + * + * @return The name of the property. + */ + public String getKey() { + return key; + } + + /** + * Returns the value of the property. + * + * @return The value of the property. + */ + public Object getValue() { + return value; + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/json/DefaultJsopHandler.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/json/DefaultJsopHandler.java new file mode 100644 index 00000000000..39243017086 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/json/DefaultJsopHandler.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl.json; + +/** + * The event callback of the parser. + * + *

    + * Each event callback has an empty default implementation. An implementor may choose the appropriate methods to + * overwrite. + *

    + */ +public class DefaultJsopHandler { + + /** + * Event: A node has been added. + * + * @param parentPath The path where the node was added to. + * @param name The name of the added node. + */ + public void nodeAdded(String parentPath, String name) { + // No-op + } + + /** + * Event: A node was copied. + * + * @param rootPath The root path where the copy took place. + * @param oldPath The old path of the node (relative to the root path). + * @param newPath The new path of the node (relative to the root path). + */ + public void nodeCopied(String rootPath, String oldPath, String newPath) { + // No-op + } + + /** + * Event: A node was moved. + * + * @param rootPath The root path where the copy took place. + * @param oldPath The old path of the node (relative to the root path). + * @param newPath The new path of the node (relative to the root path). + */ + public void nodeMoved(String rootPath, String oldPath, String newPath) { + // No-op + } + + /** + * Event: A node was removed. + * + * @param parentPath The path where the node was removed from. + * @param name The name of the node. + */ + public void nodeRemoved(String parentPath, String name) { + // No-op + } + + /** + * Event: A property was set. + * + * @param path The path of the node where the property was set. + * @param key The key of the property. + * @param value The value of the property. + */ + public void propertySet(String path, String key, Object value) { + // No-op + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/json/JsonUtil.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/json/JsonUtil.java new file mode 100644 index 00000000000..c8a574b951f --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/json/JsonUtil.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl.json; + +import java.util.LinkedList; +import java.util.List; + +import org.apache.jackrabbit.mk.json.JsopBuilder; +import org.apache.jackrabbit.mk.model.tree.ChildNode; +import org.apache.jackrabbit.mk.model.tree.NodeState; +import org.apache.jackrabbit.mk.model.tree.PropertyState; +import org.apache.jackrabbit.mk.util.NameFilter; +import org.apache.jackrabbit.mk.util.NodeFilter; +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * JSON related utility class. + */ +public class JsonUtil { + + public static Object toJsonValue(String jsonValue) throws Exception { + if (jsonValue == null) { + return null; + } + + JSONObject jsonObject = new JSONObject("{dummy : " + jsonValue + "}"); + Object obj = jsonObject.get("dummy"); + return convertJsonValue(obj); + } + + private static Object convertJsonValue(Object jsonObject) throws Exception { + if (jsonObject == JSONObject.NULL) { + return null; + } + + if (jsonObject instanceof JSONArray) { + List elements = new LinkedList(); + JSONArray dummyArray = (JSONArray) jsonObject; + for (int i = 0; i < dummyArray.length(); ++i) { + Object raw = dummyArray.get(i); + Object parsed = convertJsonValue(raw); + elements.add(parsed); + } + return elements; + } + + return jsonObject; + } + + // Most of this method borrowed from MicroKernelImpl#toJson. It'd be nice if + // this somehow consolidated with MicroKernelImpl#toJson. + public static void toJson(JsopBuilder builder, NodeState node, int depth, + int offset, int maxChildNodes, boolean inclVirtualProps, NodeFilter filter) { + + for (PropertyState property : node.getProperties()) { + if (filter == null || filter.includeProperty(property.getName())) { + builder.key(property.getName()).encodedValue(property.getEncodedValue()); + } + } + + long childCount = node.getChildNodeCount(); + if (inclVirtualProps) { + if (filter == null || filter.includeProperty(":childNodeCount")) { + // :childNodeCount is by default always included + // unless it is explicitly excluded in the filter + builder.key(":childNodeCount").value(childCount); + } + } + + if (childCount <= 0 || depth < 0) { + return; + } + + if (filter != null) { + NameFilter childFilter = filter.getChildNodeFilter(); + if (childFilter != null && !childFilter.containsWildcard()) { + // Optimization for large child node lists: + // no need to iterate over the entire child node list if the filter + // does not include wildcards + int count = maxChildNodes == -1 ? Integer.MAX_VALUE : maxChildNodes; + for (String name : childFilter.getInclusionPatterns()) { + NodeState child = node.getChildNode(name); + if (child != null) { + boolean incl = true; + for (String exclName : childFilter.getExclusionPatterns()) { + if (name.equals(exclName)) { + incl = false; + break; + } + } + if (incl) { + if (count-- <= 0) { + break; + } + builder.key(name).object(); + if (depth > 0) { + toJson(builder, child, depth - 1, 0, maxChildNodes, inclVirtualProps, filter); + } + builder.endObject(); + } + } + } + return; + } + } + + int count = maxChildNodes; + if (count != -1 && filter != null && filter.getChildNodeFilter() != null) { + // Specific maxChildNodes limit and child node filter + count = -1; + } + int numSiblings = 0; + for (ChildNode entry : node.getChildNodeEntries(offset, count)) { + + if (filter == null || filter.includeNode(entry.getName())) { + if (maxChildNodes != -1 && ++numSiblings > maxChildNodes) { + break; + } + builder.key(entry.getName()).object(); + if (depth > 0) { + toJson(builder, entry.getNode(), depth - 1, 0, maxChildNodes, inclVirtualProps, filter); + } + builder.endObject(); + } + } + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/json/JsopParser.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/json/JsopParser.java new file mode 100644 index 00000000000..d23701b2c2d --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/json/JsopParser.java @@ -0,0 +1,200 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl.json; + +import javax.xml.parsers.SAXParser; + +import org.apache.jackrabbit.mk.json.JsopReader; +import org.apache.jackrabbit.mk.json.JsopTokenizer; +import org.apache.jackrabbit.oak.commons.PathUtils; + +/** + * An event based parser for JSOP. + * + *

    + * This parser is similar to a {@link SAXParser} using a callback ({@code DefaultHandler}) to inform about certain + * events during parsing,i.e. node was added, node was removed, etc. This relieves the implementor from the burden of + * performing a semantic analysis of token which are being parsed. + *

    + * + *

    + * The underlying token parser is the {@link JsopTokenizer}. + *

    + */ +public class JsopParser { + + private final DefaultJsopHandler defaultHandler; + private final String path; + private final JsopTokenizer tokenizer; + + /** + * Constructs a new {@link JsopParser} + * + * @param path The root path of the JSON diff. + * @param jsonDiff The JSON diff. + * @param defaultHandler The {@link DefaultJsopHandler} to use. + */ + public JsopParser(String path, String jsonDiff, DefaultJsopHandler defaultHandler) { + this.path = path; + this.defaultHandler = defaultHandler; + tokenizer = new JsopTokenizer(jsonDiff); + } + + /** + * Parses the JSON diff. + * + * @throws Exception If an error occurred while parsing. + */ + public void parse() throws Exception { + if (path.length() > 0 && !PathUtils.isAbsolute(path)) { + throw new IllegalArgumentException("Absolute path expected: " + path); + } + + while (true) { + int token = tokenizer.read(); + + if (token == JsopReader.END) { + break; + } + + switch (token) { + case '+': { + parseOpAdded(path); + break; + } + case '*': { + parseOpCopied(); + break; + } + case '>': { + parseOpMoved(); + break; + } + case '^': { + parseOpSet(); + break; + } + case '-': { + parseOpRemoved(); + break; + } + default: + throw new IllegalStateException("Unknown operation: " + (char) token); + } + } + } + + private void parseOpAdded(String currentPath) throws Exception { + String subPath = tokenizer.readString(); + tokenizer.read(':'); + String path = PathUtils.concat(currentPath, subPath); + + if (tokenizer.matches('{')) { + String parentPath = PathUtils.denotesRoot(path) ? "" : PathUtils.getParentPath(path); + String nodeName = PathUtils.denotesRoot(path) ? "/" : PathUtils.getName(path); + defaultHandler.nodeAdded(parentPath, nodeName); + + if (!tokenizer.matches('}')) { + do { + int pos = tokenizer.getLastPos(); + String propName = tokenizer.readString(); + tokenizer.read(':'); + + if (tokenizer.matches('{')) { // Nested node. + // Reset to last pos as parseOpAdded expected the whole JSON. + tokenizer.setPos(pos); + tokenizer.read(); + parseOpAdded(path); + } + else { // Property. + String valueAsString = tokenizer.readRawValue().trim(); + Object value = JsonUtil.toJsonValue(valueAsString); + defaultHandler.propertySet(path, propName, value); + } + } while (tokenizer.matches(',')); + + tokenizer.read('}'); // explicitly close the bracket + } + } + } + + private void parseOpCopied() throws Exception { + int pos = tokenizer.getLastPos(); + String subPath = tokenizer.readString(); + String srcPath = PathUtils.concat(path, subPath); + if (!PathUtils.isAbsolute(srcPath)) { + throw new Exception("Absolute path expected: " + srcPath + ", pos: " + pos); + } + tokenizer.read(':'); + String targetPath = tokenizer.readString(); + if (!PathUtils.isAbsolute(targetPath)) { + targetPath = PathUtils.concat(path, targetPath); + if (!PathUtils.isAbsolute(targetPath)) { + throw new Exception("Absolute path expected: " + targetPath + ", pos: " + pos); + } + } + defaultHandler.nodeCopied(path, srcPath, targetPath); + } + + private void parseOpMoved() throws Exception { + int pos = tokenizer.getLastPos(); + String subPath = tokenizer.readString(); + String srcPath = PathUtils.concat(path, subPath); + if (!PathUtils.isAbsolute(srcPath)) { + throw new Exception("Absolute path expected: " + srcPath + ", pos: " + pos); + } + tokenizer.read(':'); + pos = tokenizer.getLastPos(); + String targetPath = tokenizer.readString(); + if (!PathUtils.isAbsolute(targetPath)) { + targetPath = PathUtils.concat(path, targetPath); + if (!PathUtils.isAbsolute(targetPath)) { + throw new Exception("absolute path expected: " + targetPath + ", pos: " + pos); + } + } + defaultHandler.nodeMoved(path, srcPath, targetPath); + } + + private void parseOpSet() throws Exception { + int pos = tokenizer.getLastPos(); + String subPath = tokenizer.readString(); + tokenizer.read(':'); + String value; + if (tokenizer.matches(JsopReader.NULL)) { + value = null; + } else { + value = tokenizer.readRawValue().trim(); + } + String targetPath = PathUtils.concat(path, subPath); + if (!PathUtils.isAbsolute(targetPath)) { + throw new Exception("Absolute path expected: " + targetPath + ", pos: " + pos); + } + String parentPath = PathUtils.getParentPath(targetPath); + String propName = PathUtils.getName(targetPath); + defaultHandler.propertySet(parentPath, propName, JsonUtil.toJsonValue(value)); + } + + private void parseOpRemoved() throws Exception { + int pos = tokenizer.getLastPos(); + String subPath = tokenizer.readString(); + String targetPath = PathUtils.concat(path, subPath); + if (!PathUtils.isAbsolute(targetPath)) { + throw new Exception("Absolute path expected: " + targetPath + ", pos: " + pos); + } + defaultHandler.nodeRemoved(path, subPath); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/model/CommitBuilder.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/model/CommitBuilder.java new file mode 100644 index 00000000000..4cc8621be42 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/model/CommitBuilder.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.mongomk.impl.model; + +import org.apache.jackrabbit.mongomk.api.model.Commit; +import org.apache.jackrabbit.mongomk.impl.instruction.AddNodeInstructionImpl; +import org.apache.jackrabbit.mongomk.impl.instruction.CopyNodeInstructionImpl; +import org.apache.jackrabbit.mongomk.impl.instruction.MoveNodeInstructionImpl; +import org.apache.jackrabbit.mongomk.impl.instruction.RemoveNodeInstructionImpl; +import org.apache.jackrabbit.mongomk.impl.instruction.SetPropertyInstructionImpl; +import org.apache.jackrabbit.mongomk.impl.json.DefaultJsopHandler; +import org.apache.jackrabbit.mongomk.impl.json.JsopParser; +import org.apache.jackrabbit.mongomk.util.MongoUtil; + +/** + * A builder to convert a JSOP + * diff into a {@link Commit}. + */ +public class CommitBuilder { + + /** + * Creates and returns a {@link Commit} without a base revision id. + * + * @param path The root path of the {@code Commit}. + * @param diff The {@code JSOP} diff of the {@code Commit}. + * @param message The message of the {@code Commit}. + * + * @return The {@code Commit}. + * @throws Exception If an error occurred while creating the {@code Commit}. + */ + public static Commit build(String path, String diff, String message) + throws Exception { + return CommitBuilder.build(path, diff, null, message); + } + + /** + * Creates and returns a {@link Commit}. + * + * @param path The root path of the {@code Commit}. + * @param diff The {@code JSOP} diff of the {@code Commit}. + * @param revisionId The revision id the commit is based on. + * @param message The message of the {@code Commit}. + * + * @return The {@code Commit}. + * @throws Exception If an error occurred while creating the {@code Commit}. + */ + public static Commit build(String path, String diff, String revisionId, + String message) throws Exception { + CommitImpl commit = new CommitImpl(path, diff, message); + commit.setBaseRevisionId(MongoUtil.toMongoRepresentation(revisionId)); + CommitHandler commitHandler = new CommitHandler(commit); + JsopParser jsopParser = new JsopParser(path, diff, commitHandler); + jsopParser.parse(); + return commit; + } + + /** + * The {@link DefaultJaopHandler} for the {@code JSOP} diff. + */ + private static class CommitHandler extends DefaultJsopHandler { + private final CommitImpl commit; + + CommitHandler(CommitImpl commit) { + this.commit = commit; + } + + @Override + public void nodeAdded(String parentPath, String name) { + commit.addInstruction(new AddNodeInstructionImpl(parentPath, name)); + } + + @Override + public void nodeCopied(String rootPath, String oldPath, String newPath) { + commit.addInstruction(new CopyNodeInstructionImpl(rootPath, oldPath, newPath)); + } + + @Override + public void nodeMoved(String rootPath, String oldPath, String newPath) { + commit.addInstruction(new MoveNodeInstructionImpl(rootPath, oldPath, newPath)); + } + + @Override + public void nodeRemoved(String parentPath, String name) { + commit.addInstruction(new RemoveNodeInstructionImpl(parentPath, name)); + } + + @Override + public void propertySet(String path, String key, Object value) { + commit.addInstruction(new SetPropertyInstructionImpl(path, key, value)); + } + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/model/CommitImpl.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/model/CommitImpl.java new file mode 100644 index 00000000000..5e857422be9 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/model/CommitImpl.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl.model; + +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +import org.apache.jackrabbit.mongomk.api.instruction.Instruction; +import org.apache.jackrabbit.mongomk.api.model.Commit; + +/** + * Implementation of {@link Commit}. + */ +public class CommitImpl implements Commit { + + private final String diff; + private final List instructions; + private final String message; + private final String path; + private final long timestamp; + + private Long baseRevisionId; + private String branchId; + private Long revisionId; + + /** + * Constructs a new {@code CommitImpl}. + * + * @param path The path. + * @param diff The diff. + * @param message The message. + */ + public CommitImpl(String path, String diff, String message) { + this.path = path; + this.diff = diff; + this.message = message; + instructions = new LinkedList(); + timestamp = new Date().getTime(); + } + + /** + * Adds the given {@link Instruction}. + * + * @param instruction The {@code Instruction}. + */ + public void addInstruction(Instruction instruction) { + instructions.add(instruction); + } + + @Override + public Long getBaseRevisionId() { + return baseRevisionId; + } + + public void setBaseRevisionId(Long baseRevisionId) { + this.baseRevisionId = baseRevisionId; + } + + @Override + public String getBranchId() { + return branchId; + } + + public void setBranchId(String branchId) { + this.branchId = branchId; + } + + @Override + public String getDiff() { + return diff; + } + + @Override + public List getInstructions() { + return Collections.unmodifiableList(instructions); + } + + @Override + public String getMessage() { + return message; + } + + @Override + public String getPath() { + return path; + } + + @Override + public Long getRevisionId() { + return revisionId; + } + + @Override + public void setRevisionId(Long revisionId) { + this.revisionId = revisionId; + } + + @Override + public long getTimestamp() { + return timestamp; + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/model/NodeImpl.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/model/NodeImpl.java new file mode 100644 index 00000000000..a651ba352f7 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/model/NodeImpl.java @@ -0,0 +1,265 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl.model; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.jackrabbit.mk.model.ChildNodeEntry; +import org.apache.jackrabbit.mk.model.NodeDiffHandler; +import org.apache.jackrabbit.mk.util.RangeIterator; +import org.apache.jackrabbit.mongomk.api.model.Node; +import org.apache.jackrabbit.oak.commons.PathUtils; + +/** + * Implementation of {@link Node}. + */ +public class NodeImpl implements Node { + + private static final List EMPTY = Collections.emptyList(); + + private Map childEntries; + private String path; + private Map properties; + private Long revisionId; + + /** + * Constructs a new {@code NodeImpl}. + * + * @param path The path. + */ + public NodeImpl(String path) { + this.path = path; + this.childEntries = new HashMap(); + this.properties = new HashMap(); + } + + /** + * Adds the given {@link Node} as child. + * + * @param child The {@code node} to add. + */ + public void addChildNodeEntry(Node child) { + String childName = PathUtils.getName(child.getPath()); + childEntries.put(childName, child); + } + + @Override + public Node getChildNodeEntry(String name) { + return childEntries.get(name); + } + + @Override + public int getChildNodeCount() { + return childEntries.size(); + } + + @Override + public Iterator getChildNodeEntries(int offset, int count) { + if (offset < 0 || count < -1) { + throw new IllegalArgumentException(); + } + + if (offset == 0 && count == -1) { + return childEntries.values().iterator(); + } + + if (offset >= childEntries.size() || count == 0) { + return EMPTY.iterator(); + } + + if (count == -1 || (offset + count) > childEntries.size()) { + count = childEntries.size() - offset; + } + + return new RangeIterator(childEntries.values().iterator(), offset, count); + } + + public void removeChildNodeEntry(String name) { + childEntries.remove(name); + } + + public void addProperty(String key, Object value) { + properties.put(key, value); + } + + @Override + public Map getProperties() { + return properties; + } + + @Override + public String getPath() { + return path; + } + + @Override + public Long getRevisionId() { + return revisionId; + } + + public void setRevisionId(Long revisionId) { + this.revisionId = revisionId; + } + + @Override + public void diff(Node other, NodeDiffHandler handler) { + + // Note: Most of this functionality is mirrored from AbstractNode with + // the hopes that the two functionality can be consolidated at some point. + + // Compare properties + Map oldProps = getProperties(); + Map newProps = other.getProperties(); + + for (Map.Entry entry : oldProps.entrySet()) { + String name = entry.getKey(); + Object val = oldProps.get(name); + Object newVal = newProps.get(name); + if (newVal == null) { + handler.propDeleted(name, val.toString()); + } else { + if (!val.equals(newVal)) { + handler.propChanged(name, val.toString(), newVal.toString()); + } + } + } + + for (Map.Entry entry : newProps.entrySet()) { + String name = entry.getKey(); + if (!oldProps.containsKey(name)) { + handler.propAdded(name, entry.getValue().toString()); + } + } + + // Compare child node entries + for (Iterator it = getChildNodeEntries(0, -1); it.hasNext(); ) { + Node child = it.next(); + String childName = PathUtils.getName(child.getPath()); + Node newChild = other.getChildNodeEntry(childName); + if (newChild == null) { + handler.childNodeDeleted(new ChildNodeEntry(childName, null)); + } else { + if (!child.equals(newChild)) { + handler.childNodeChanged(new ChildNodeEntry(childName, null), + null /*newId*/); + } + } + } + + for (Iterator it = other.getChildNodeEntries(0, -1); it.hasNext(); ) { + Node child = it.next(); + String childName = PathUtils.getName(child.getPath()); + if (getChildNodeEntry(childName) == null) { + handler.childNodeAdded(new ChildNodeEntry(childName, null)); + } + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = (prime * result) + ((childEntries == null) ? 0 : childEntries.hashCode()); + result = (prime * result) + ((path == null) ? 0 : path.hashCode()); + result = (prime * result) + ((properties == null) ? 0 : properties.hashCode()); + result = (prime * result) + ((revisionId == null) ? 0 : revisionId.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + NodeImpl other = (NodeImpl) obj; + if (childEntries == null) { + if (other.childEntries != null) { + return false; + } + } else if (!childEntries.equals(other.childEntries)) { + return false; + } + if (path == null) { + if (other.path != null) { + return false; + } + } else if (!path.equals(other.path)) { + return false; + } + if (properties == null) { + if (other.properties != null) { + return false; + } + } else if (!properties.equals(other.properties)) { + return false; + } + if (revisionId == null) { + if (other.revisionId != null) { + return false; + } + } else if (!revisionId.equals(other.revisionId)) { + return false; + } + return true; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("NodeImpl "); + builder.append("path="); + builder.append(path); + + if (revisionId != null) { + builder.append(", revisionId="); + builder.append(revisionId); + } + + if (!childEntries.isEmpty()) { + builder.append(", children=["); + Set childNames = childEntries.keySet(); + int childCount = childNames.size(); + int i = 0; + for (String childName : childEntries.keySet()) { + if (i++ < childCount - 1) { + builder.append(childName + ", "); + } else { + builder.append(childName); + } + } + builder.append("]"); + } + + if (!properties.isEmpty()) { + builder.append(", properties="); + builder.append(properties); + } + + return builder.toString(); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/model/tree/MongoNodeDelta.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/model/tree/MongoNodeDelta.java new file mode 100644 index 00000000000..c4aa86d635e --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/model/tree/MongoNodeDelta.java @@ -0,0 +1,238 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl.model.tree; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.jackrabbit.mk.model.Id; +import org.apache.jackrabbit.mk.model.tree.NodeState; +import org.apache.jackrabbit.mk.model.tree.NodeStateDiff; +import org.apache.jackrabbit.mk.model.tree.NodeStore; +import org.apache.jackrabbit.mk.model.tree.PropertyState; + +/** + * Note: Most of this functionality is mirrored from NodeDelta with the hopes + * that the two functionality can be consolidated at some point. + */ +public class MongoNodeDelta { + + public static enum ConflictType { + /** + * same property has been added or set, but with differing values + */ + PROPERTY_VALUE_CONFLICT, + /** + * child nodes with identical name have been added or modified, but + * with differing id's; the corresponding node subtrees are hence differing + * and potentially conflicting. + */ + NODE_CONTENT_CONFLICT, + /** + * a modified property has been deleted + */ + REMOVED_DIRTY_PROPERTY_CONFLICT, + /** + * a child node entry pointing to a modified subtree has been deleted + */ + REMOVED_DIRTY_NODE_CONFLICT + } + + private final NodeStore provider; + + private final NodeState node1; + + Map addedProperties = new HashMap(); + Map removedProperties = new HashMap(); + Map changedProperties = new HashMap(); + + Map addedChildNodes = new HashMap(); + Map removedChildNodes = new HashMap(); + Map changedChildNodes = new HashMap(); + + public MongoNodeDelta(NodeStore provider, NodeState node1, NodeState node2) { + this.provider = provider; + this.node1 = node1; + this.provider.compare(node1, node2, new DiffHandler()); + } + + public Map getAddedProperties() { + return addedProperties; + } + + public Map getRemovedProperties() { + return removedProperties; + } + + public Map getChangedProperties() { + return changedProperties; + } + + public Map getAddedChildNodes() { + return addedChildNodes; + } + + public Map getRemovedChildNodes() { + return removedChildNodes; + } + + public Map getChangedChildNodes() { + return changedChildNodes; + } + + public boolean conflictsWith(MongoNodeDelta other) { + return !listConflicts(other).isEmpty(); + } + + public List listConflicts(MongoNodeDelta other) { + // assume that both delta's were built using the *same* base node revision + if (!node1.equals(other.node1)) { + throw new IllegalArgumentException("other and this NodeDelta object are expected to share common node1 instance"); + } + + List conflicts = new ArrayList(); + + // properties + + Map otherAddedProps = other.getAddedProperties(); + for (Map.Entry added : addedProperties.entrySet()) { + String otherValue = otherAddedProps.get(added.getKey()); + if (otherValue != null && !added.getValue().equals(otherValue)) { + // same property added with conflicting values + conflicts.add(new Conflict(ConflictType.PROPERTY_VALUE_CONFLICT, added.getKey())); + } + } + + Map otherChangedProps = other.getChangedProperties(); + Map otherRemovedProps = other.getRemovedProperties(); + for (Map.Entry changed : changedProperties.entrySet()) { + String otherValue = otherChangedProps.get(changed.getKey()); + if (otherValue != null && !changed.getValue().equals(otherValue)) { + // same property changed with conflicting values + conflicts.add(new Conflict(ConflictType.PROPERTY_VALUE_CONFLICT, changed.getKey())); + } + if (otherRemovedProps.containsKey(changed.getKey())) { + // changed property has been removed + conflicts.add(new Conflict(ConflictType.REMOVED_DIRTY_PROPERTY_CONFLICT, changed.getKey())); + } + } + + for (Map.Entry removed : removedProperties.entrySet()) { + if (otherChangedProps.containsKey(removed.getKey())) { + // removed property has been changed + conflicts.add(new Conflict(ConflictType.REMOVED_DIRTY_PROPERTY_CONFLICT, removed.getKey())); + } + } + + // child node entries + + //Map otherAddedChildNodes = other.getAddedChildNodes(); + Map otherAddedChildNodes = other.getAddedChildNodes(); + for (Map.Entry added : addedChildNodes.entrySet()) { + NodeState otherValue = otherAddedChildNodes.get(added.getKey()); + if (otherValue != null && !added.getValue().equals(otherValue)) { + // same child node entry added with different target id's + conflicts.add(new Conflict(ConflictType.NODE_CONTENT_CONFLICT, added.getKey())); + } + } + + //Map otherChangedChildNodes = other.getChangedChildNodes(); + Map otherChangedChildNodes = other.getChangedChildNodes(); + Map otherRemovedChildNodes = other.getRemovedChildNodes(); + for (Map.Entry changed : changedChildNodes.entrySet()) { + NodeState otherValue = otherChangedChildNodes.get(changed.getKey()); + if (otherValue != null && !changed.getValue().equals(otherValue)) { + // same child node entry changed with different target id's + conflicts.add(new Conflict(ConflictType.NODE_CONTENT_CONFLICT, changed.getKey())); + } + if (otherRemovedChildNodes.containsKey(changed.getKey())) { + // changed child node entry has been removed + conflicts.add(new Conflict(ConflictType.REMOVED_DIRTY_NODE_CONFLICT, changed.getKey())); + } + } + + for (Map.Entry removed : removedChildNodes.entrySet()) { + if (otherChangedChildNodes.containsKey(removed.getKey())) { + // removed child node entry has been changed + conflicts.add(new Conflict(ConflictType.REMOVED_DIRTY_NODE_CONFLICT, removed.getKey())); + } + } + + return conflicts; + } + + //--------------------------------------------------------< inner classes > + + private class DiffHandler implements NodeStateDiff { + + @Override + public void propertyAdded(PropertyState after) { + addedProperties.put(after.getName(), after.getEncodedValue()); + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) { + changedProperties.put(after.getName(), after.getEncodedValue()); + } + + @Override + public void propertyDeleted(PropertyState before) { + removedProperties.put(before.getName(), before.getEncodedValue()); + } + + @Override + public void childNodeAdded(String name, NodeState after) { + addedChildNodes.put(name, after); + } + + @Override + public void childNodeChanged( + String name, NodeState before, NodeState after) { + changedChildNodes.put(name, after /*provider.getId(after)*/); + } + + @Override + public void childNodeDeleted(String name, NodeState before) { + removedChildNodes.put(name, null /*provider.getId(before)*/); + } + } + + public static class Conflict { + + final ConflictType type; + final String name; + + /** + * @param type conflict type + * @param name name of conflicting property or child node + */ + Conflict(ConflictType type, String name) { + this.type = type; + this.name = name; + } + + public ConflictType getType() { + return type; + } + + public String getName() { + return name; + } + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/model/tree/MongoNodeState.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/model/tree/MongoNodeState.java new file mode 100644 index 00000000000..f439baa20c8 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/model/tree/MongoNodeState.java @@ -0,0 +1,147 @@ +package org.apache.jackrabbit.mongomk.impl.model.tree; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; + +import org.apache.jackrabbit.mk.json.JsopBuilder; +import org.apache.jackrabbit.mk.model.tree.AbstractChildNode; +import org.apache.jackrabbit.mk.model.tree.AbstractNodeState; +import org.apache.jackrabbit.mk.model.tree.AbstractPropertyState; +import org.apache.jackrabbit.mk.model.tree.ChildNode; +import org.apache.jackrabbit.mk.model.tree.NodeState; +import org.apache.jackrabbit.mk.model.tree.PropertyState; +import org.apache.jackrabbit.mongomk.api.model.Node; +import org.apache.jackrabbit.oak.commons.PathUtils; + +/** + * This dummy NodeStore implementation is needed in order to be able to reuse + * Oak's DiffBuilder in MongoMK. + */ +public class MongoNodeState extends AbstractNodeState { + + private final Node node; + + /** + * Create a node state with the supplied node. + * + * @param node Node. + */ + public MongoNodeState(Node node) { + this.node = node; + } + + /** + * Returns the underlying node. + * + * @return The underlying node. + */ + public Node unwrap() { + return node; + } + + @Override + public Iterable getProperties() { + return new Iterable() { + @Override + public Iterator iterator() { + final Iterator> iterator = + node.getProperties().entrySet().iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + @Override + public PropertyState next() { + Map.Entry entry = iterator.next(); + Object value = entry.getValue(); + String valueStr = null; + if (value instanceof String) { + valueStr = JsopBuilder.encode(value.toString()); + } else { + valueStr = value.toString(); + } + return new SimplePropertyState(entry.getKey(), valueStr); + } + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + + @Override + public Iterable getChildNodeEntries(final long offset, + final int count) { + if (count < -1) { + throw new IllegalArgumentException("Illegal count: " + count); + } + + if (offset > Integer.MAX_VALUE) { + return Collections.emptyList(); + } + + return new Iterable() { + @Override + public Iterator iterator() { + final Iterator iterator = + node.getChildNodeEntries((int) offset, count); + return new Iterator() { + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + @Override + public ChildNode next() { + return getChildNodeEntry(iterator.next()); + } + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + + private ChildNode getChildNodeEntry(final Node entry) { + + return new AbstractChildNode() { + @Override + public String getName() { + return PathUtils.getName(entry.getPath()); + } + @Override + public NodeState getNode() { + try { + return new MongoNodeState(entry); + } catch (Exception e) { + throw new RuntimeException("Unexpected error", e); + } + } + }; + } + + private static class SimplePropertyState extends AbstractPropertyState { + private final String name; + private final String value; + + public SimplePropertyState(String name, String value) { + this.name = name; + this.value = value; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getEncodedValue() { + return value; + } + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/model/tree/MongoNodeStore.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/model/tree/MongoNodeStore.java new file mode 100644 index 00000000000..d95fd151409 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/impl/model/tree/MongoNodeStore.java @@ -0,0 +1,67 @@ +package org.apache.jackrabbit.mongomk.impl.model.tree; + +import org.apache.jackrabbit.mk.model.ChildNodeEntry; +import org.apache.jackrabbit.mk.model.Id; +import org.apache.jackrabbit.mk.model.NodeDiffHandler; +import org.apache.jackrabbit.mk.model.tree.NodeState; +import org.apache.jackrabbit.mk.model.tree.NodeStateDiff; +import org.apache.jackrabbit.mk.model.tree.NodeStore; +import org.apache.jackrabbit.mongomk.api.model.Node; + +/** + * This dummy NodeStore implementation is needed in order to be able to reuse + * Oak's DiffBuilder in MongoMK. + */ +public class MongoNodeStore implements NodeStore { + + @Override + public NodeState getRoot() { + return null; + } + + @Override + public void compare(final NodeState before, final NodeState after, + final NodeStateDiff diff) { + + Node beforeNode = ((MongoNodeState)before).unwrap(); + Node afterNode = ((MongoNodeState)after).unwrap(); + + beforeNode.diff(afterNode, new NodeDiffHandler() { + @Override + public void propAdded(String propName, String value) { + diff.propertyAdded(after.getProperty(propName)); + } + + @Override + public void propChanged(String propName, String oldValue, + String newValue) { + diff.propertyChanged(before.getProperty(propName), + after.getProperty(propName)); + } + + @Override + public void propDeleted(String propName, String value) { + diff.propertyDeleted(before.getProperty(propName)); + } + + @Override + public void childNodeAdded(ChildNodeEntry added) { + String name = added.getName(); + diff.childNodeAdded(name, after.getChildNode(name)); + } + + @Override + public void childNodeDeleted(ChildNodeEntry deleted) { + String name = deleted.getName(); + diff.childNodeDeleted(name, before.getChildNode(name)); + } + + @Override + public void childNodeChanged(ChildNodeEntry changed, Id newId) { + String name = changed.getName(); + diff.childNodeChanged(name, before.getChildNode(name), + after.getChildNode(name)); + } + }); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/model/CommitCommandInstructionVisitor.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/model/CommitCommandInstructionVisitor.java new file mode 100644 index 00000000000..550c7c18a38 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/model/CommitCommandInstructionVisitor.java @@ -0,0 +1,341 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.model; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.jackrabbit.mongomk.action.FetchNodesAction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.AddNodeInstruction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.CopyNodeInstruction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.MoveNodeInstruction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.RemoveNodeInstruction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.SetPropertyInstruction; +import org.apache.jackrabbit.mongomk.api.instruction.InstructionVisitor; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.impl.command.NodeExistsCommand; +import org.apache.jackrabbit.oak.commons.PathUtils; + +/** + * This class reads in the instructions generated from JSON, applies basic checks + * and creates a node map for {@code CommitCommandMongo} to work on later. + */ +public class CommitCommandInstructionVisitor implements InstructionVisitor { + + private final long headRevisionId; + private final MongoConnection mongoConnection; + private final Map pathNodeMap; + + private String branchId; + + /** + * Creates {@code CommitCommandInstructionVisitor} + * + * @param mongoConnection Mongo connection. + * @param headRevisionId Head revision. + */ + public CommitCommandInstructionVisitor(MongoConnection mongoConnection, + long headRevisionId) { + this.mongoConnection = mongoConnection; + this.headRevisionId = headRevisionId; + pathNodeMap = new HashMap(); + } + + /** + * Sets the branch id associated with the commit. It can be null. + * + * @param branchId Branch id or null. + */ + public void setBranchId(String branchId) { + this.branchId = branchId; + } + + /** + * Returns the generated node map after visit methods are called. + * + * @return Node map. + */ + public Map getPathNodeMap() { + return pathNodeMap; + } + + @Override + public void visit(AddNodeInstruction instruction) { + String nodePath = instruction.getPath(); + checkAbsolutePath(nodePath); + + String nodeName = PathUtils.getName(nodePath); + if (nodeName.isEmpty()) { // This happens in initial commit. + getStagedNode(nodePath); + return; + } + + String parentNodePath = PathUtils.getParentPath(nodePath); + NodeMongo parent = getStoredNode(parentNodePath); + if (parent.childExists(nodeName)) { + throw new RuntimeException("There's already a child node with name '" + nodeName + "'"); + } + getStagedNode(nodePath); + parent.addChild(nodeName); + } + + @Override + public void visit(SetPropertyInstruction instruction) { + String key = instruction.getKey(); + Object value = instruction.getValue(); + NodeMongo node = getStoredNode(instruction.getPath()); + if (value == null) { + node.removeProp(key); + } else { + node.addProperty(key, value); + } + } + + @Override + public void visit(RemoveNodeInstruction instruction) { + String nodePath = instruction.getPath(); + checkAbsolutePath(nodePath); + + String parentPath = PathUtils.getParentPath(nodePath); + String nodeName = PathUtils.getName(nodePath); + NodeMongo parent = getStoredNode(parentPath); + if (!parent.childExists(nodeName)) { + throw new RuntimeException("Node " + nodeName + + " does not exists at parent path: " + parentPath); + } + parent.removeChild(PathUtils.getName(nodePath)); + } + + @Override + public void visit(CopyNodeInstruction instruction) { + String srcPath = instruction.getSourcePath(); + checkAbsolutePath(srcPath); + + String destPath = instruction.getDestPath(); + if (!PathUtils.isAbsolute(destPath)) { + destPath = PathUtils.concat(instruction.getPath(), destPath); + checkAbsolutePath(destPath); + } + + String srcParentPath = PathUtils.getParentPath(srcPath); + String srcNodeName = PathUtils.getName(srcPath); + + String destParentPath = PathUtils.getParentPath(destPath); + String destNodeName = PathUtils.getName(destPath); + + NodeMongo srcParent = getStoredNode(srcParentPath); + if (!srcParent.childExists(srcNodeName)) { + throw new NotFoundException(srcPath); + } + NodeMongo destParent = getStoredNode(destParentPath); + if (destParent.childExists(destNodeName)) { + throw new RuntimeException("Node already exists at copy destination path: " + destPath); + } + + // First, copy the existing nodes. + List nodesToCopy = new FetchNodesAction(mongoConnection, + srcPath, true, headRevisionId).execute(); + for (NodeMongo nodeMongo : nodesToCopy) { + String oldPath = nodeMongo.getPath(); + String oldPathRel = PathUtils.relativize(srcPath, oldPath); + String newPath = PathUtils.concat(destPath, oldPathRel); + + nodeMongo.setPath(newPath); + nodeMongo.removeField("_id"); + pathNodeMap.put(newPath, nodeMongo); + } + + // Then, copy any staged changes. + NodeMongo srcNode = getStoredNode(srcPath); + NodeMongo destNode = getStagedNode(destPath); + copyStagedChanges(srcNode, destNode); + + // Finally, add to destParent. + pathNodeMap.put(destPath, destNode); + destParent.addChild(destNodeName); + } + + @Override + public void visit(MoveNodeInstruction instruction) { + String srcPath = instruction.getSourcePath(); + String destPath = instruction.getDestPath(); + if (PathUtils.isAncestor(srcPath, destPath)) { + throw new RuntimeException("Target path cannot be descendant of source path: " + + destPath); + } + + String srcParentPath = PathUtils.getParentPath(srcPath); + String srcNodeName = PathUtils.getName(srcPath); + + String destParentPath = PathUtils.getParentPath(destPath); + String destNodeName = PathUtils.getName(destPath); + + NodeMongo srcParent = getStoredNode(srcParentPath); + if (!srcParent.childExists(srcNodeName)) { + throw new NotFoundException(srcPath); + } + + NodeMongo destParent = getStoredNode(destParentPath); + if (destParent.childExists(destNodeName)) { + throw new RuntimeException("Node already exists at move destination path: " + destPath); + } + + // First, copy the existing nodes. + List nodesToCopy = new FetchNodesAction(mongoConnection, + srcPath, true, headRevisionId).execute(); + for (NodeMongo nodeMongo : nodesToCopy) { + String oldPath = nodeMongo.getPath(); + String oldPathRel = PathUtils.relativize(srcPath, oldPath); + String newPath = PathUtils.concat(destPath, oldPathRel); + + nodeMongo.setPath(newPath); + nodeMongo.removeField("_id"); + pathNodeMap.put(newPath, nodeMongo); + } + + // Then, copy any staged changes. + NodeMongo srcNode = getStoredNode(srcPath); + NodeMongo destNode = getStagedNode(destPath); + copyStagedChanges(srcNode, destNode); + + // Finally, add to destParent and remove from srcParent. + getStagedNode(destPath); + destParent.addChild(destNodeName); + srcParent.removeChild(srcNodeName); + } + + private void checkAbsolutePath(String srcPath) { + if (!PathUtils.isAbsolute(srcPath)) { + throw new RuntimeException("Absolute path expected: " + srcPath); + } + } + + private NodeMongo getStagedNode(String path) { + NodeMongo node = pathNodeMap.get(path); + if (node == null) { + node = new NodeMongo(); + node.setPath(path); + pathNodeMap.put(path, node); + } + return node; + } + + private NodeMongo getStoredNode(String path) { + NodeMongo node = pathNodeMap.get(path); + if (node != null) { + return node; + } + + // First need to check that the path is indeed valid. + NodeExistsCommand existCommand = new NodeExistsCommand(mongoConnection, + path, headRevisionId); + existCommand.setBranchId(branchId); + boolean exists = false; + try { + exists = existCommand.execute(); + } catch (Exception ignore) {} + + if (!exists) { + throw new NotFoundException(path); + } + + // Fetch the node without its descendants. + FetchNodesAction query = new FetchNodesAction(mongoConnection, + path, false /*fetchDescendants*/, headRevisionId); + query.setBranchId(branchId); + List nodes = query.execute(); + + if (!nodes.isEmpty()) { + node = nodes.get(0); + node.removeField("_id"); + pathNodeMap.put(path, node); + } + + return node; + } + + private void copyStagedChanges(NodeMongo srcNode, NodeMongo destNode) { + + // Copy staged changes at the top level. + copyAddedNodes(srcNode, destNode); + copyRemovedNodes(srcNode, destNode); + copyAddedProperties(srcNode, destNode); + copyRemovedProperties(srcNode, destNode); + + // Recursively add staged changes of the descendants. + List srcChildren = srcNode.getChildren(); + if (srcChildren == null || srcChildren.isEmpty()) { + return; + } + + for (String childName : srcChildren) { + String oldChildPath = PathUtils.concat(srcNode.getPath(), childName); + NodeMongo oldChild = getStoredNode(oldChildPath); + + String newChildPath = PathUtils.concat(destNode.getPath(), childName); + NodeMongo newChild = getStagedNode(newChildPath); + copyStagedChanges(oldChild, newChild); + } + } + + private void copyRemovedProperties(NodeMongo srcNode, NodeMongo destNode) { + Map removedProps = srcNode.getRemovedProps(); + if (removedProps == null || removedProps.isEmpty()) { + return; + } + + for (String key : removedProps.keySet()) { + destNode.removeProp(key); + } + } + + private void copyAddedNodes(NodeMongo srcNode, NodeMongo destNode) { + List addedChildren = srcNode.getAddedChildren(); + if (addedChildren == null || addedChildren.isEmpty()) { + return; + } + + for (String childName : addedChildren) { + getStagedNode(PathUtils.concat(destNode.getPath(), childName)); + destNode.addChild(childName); + } + } + + private void copyRemovedNodes(NodeMongo srcNode, NodeMongo destNode) { + List removedChildren = srcNode.getRemovedChildren(); + if (removedChildren == null || removedChildren.isEmpty()) { + return; + } + + for (String child : removedChildren) { + destNode.removeChild(child); + } + } + + private void copyAddedProperties(NodeMongo srcNode, NodeMongo destNode) { + Map addedProps = srcNode.getAddedProps(); + if (addedProps == null || addedProps.isEmpty()) { + return; + } + + for (Entry entry : addedProps.entrySet()) { + destNode.addProperty(entry.getKey(), entry.getValue()); + } + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/model/CommitMongo.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/model/CommitMongo.java new file mode 100644 index 00000000000..f41d3b449ca --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/model/CommitMongo.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.model; + +import java.util.Date; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.apache.jackrabbit.mongomk.api.instruction.Instruction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.AddNodeInstruction; +import org.apache.jackrabbit.mongomk.api.model.Commit; +import org.apache.jackrabbit.oak.commons.PathUtils; + +import com.mongodb.BasicDBObject; + +/** + * The {@code MongoDB} representation of a commit. + */ +public class CommitMongo extends BasicDBObject { + + public static final String KEY_AFFECTED_PATH = "affPaths"; + public static final String KEY_BASE_REVISION_ID = "baseRevId"; + public static final String KEY_BRANCH_ID = "branchId"; + public static final String KEY_DIFF = "diff"; + public static final String KEY_FAILED = "failed"; + public static final String KEY_MESSAGE = "msg"; + public static final String KEY_PATH = "path"; + public static final String KEY_REVISION_ID = "revId"; + public static final String KEY_TIMESTAMP = "ts"; + private static final long serialVersionUID = 6656294757102309827L; + + public static CommitMongo fromCommit(Commit commit) { + CommitMongo commitMongo = new CommitMongo(); + + String message = commit.getMessage(); + commitMongo.setMessage(message); + + String path = commit.getPath(); + commitMongo.setPath(path); + + String diff = commit.getDiff(); + commitMongo.setDiff(diff); + + Long revisionId = commit.getRevisionId(); + if (revisionId != null) { + commitMongo.setRevisionId(revisionId); + } + + String branchId = commit.getBranchId(); + if (branchId != null) { + commitMongo.setBranchId(branchId); + } + + commitMongo.setTimestamp(commit.getTimestamp()); + + Set affectedPaths = new HashSet(); + for (Instruction instruction : commit.getInstructions()) { + affectedPaths.add(instruction.getPath()); + + if (instruction instanceof AddNodeInstruction) { + affectedPaths.add(PathUtils.getParentPath(instruction.getPath())); + } + } + commitMongo.setAffectedPaths(new LinkedList(affectedPaths)); + + return commitMongo; + } + + public CommitMongo() { + setTimestamp(new Date().getTime()); + } + + @SuppressWarnings("unchecked") + public List getAffectedPaths() { + return (List) get(KEY_AFFECTED_PATH); + } + + public void setAffectedPaths(List affectedPaths) { + put(KEY_AFFECTED_PATH, affectedPaths); + } + + public long getBaseRevId() { + return getLong(KEY_BASE_REVISION_ID); + } + + public void setBaseRevId(long baseRevisionId) { + put(KEY_BASE_REVISION_ID, baseRevisionId); + } + + public String getBranchId() { + return getString(KEY_BRANCH_ID); + } + + public void setBranchId(String branchId) { + put(KEY_BRANCH_ID, branchId); + } + + public String getDiff() { + return getString(KEY_DIFF); + } + + public void setDiff(String diff) { + put(KEY_DIFF, diff); + } + + public String getMessage() { + return getString(KEY_MESSAGE); + } + + public void setMessage(String message) { + put(KEY_MESSAGE, message); + } + + public String getPath() { + return getString(KEY_PATH); + } + + public void setPath(String path) { + put(KEY_PATH, path); + } + + public long getRevisionId() { + return getLong(KEY_REVISION_ID); + } + + public void setRevisionId(long revisionId) { + put(KEY_REVISION_ID, revisionId); + } + + public boolean isFailed() { + return getBoolean(KEY_FAILED); + } + + public void setFailed() { + put(KEY_FAILED, Boolean.TRUE); + } + + public Long getTimestamp() { + return getLong(KEY_TIMESTAMP); + } + + public void setTimestamp(long timestamp) { + put(KEY_TIMESTAMP, timestamp); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/model/NodeMongo.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/model/NodeMongo.java new file mode 100644 index 00000000000..c5950097272 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/model/NodeMongo.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.mongomk.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.apache.jackrabbit.mongomk.api.model.Node; +import org.apache.jackrabbit.mongomk.impl.model.NodeImpl; +import org.apache.jackrabbit.oak.commons.PathUtils; + +import com.mongodb.BasicDBObject; + +/** + * The {@code MongoDB} representation of a node. + */ +public class NodeMongo extends BasicDBObject { + + public static final String KEY_BASE_REVISION_ID = "baseRevId"; + public static final String KEY_CHILDREN = "children"; + public static final String KEY_PATH = "path"; + public static final String KEY_PROPERTIES = "props"; + public static final String KEY_REVISION_ID = "revId"; + public static final String KEY_BRANCH_ID = "branchId"; + + private static final long serialVersionUID = 3153393934945155106L; + + private List addedChildren; + private Map addedProps; + private List removedChildren; + private Map removedProps; + + public static List toNode(Collection nodeMongos) { + List nodes = new ArrayList(nodeMongos.size()); + for (NodeMongo nodeMongo : nodeMongos) { + Node node = NodeMongo.toNode(nodeMongo); + nodes.add(node); + } + + return nodes; + } + + public static NodeImpl toNode(NodeMongo nodeMongo) { + String path = nodeMongo.getPath(); + NodeImpl nodeImpl = new NodeImpl(path); + + List childNames = nodeMongo.getChildren(); + if (childNames != null) { + for (String childName : childNames) { + String childPath = PathUtils.concat(path, childName); + NodeImpl child = new NodeImpl(childPath); + nodeImpl.addChildNodeEntry(child); + } + } + + nodeImpl.setRevisionId(nodeMongo.getRevisionId()); + for (Map.Entry entry : nodeMongo.getProperties().entrySet()) { + nodeImpl.addProperty(entry.getKey(), entry.getValue()); + } + return nodeImpl; + } + + //-------------------------------------------------------------------------- + // + // These properties are persisted to MongoDB + // + //-------------------------------------------------------------------------- + + public void setBaseRevisionId(long baseRevisionId) { + put(KEY_BASE_REVISION_ID, baseRevisionId); + } + + public String getBranchId() { + return getString(KEY_BRANCH_ID); + } + + public void setBranchId(String branchId) { + put(KEY_BRANCH_ID, branchId); + } + + @SuppressWarnings("unchecked") + public List getChildren() { + return (List)get(KEY_CHILDREN); + } + + public void setChildren(List children) { + if (children != null) { + put(KEY_CHILDREN, children); + } else { + removeField(KEY_CHILDREN); + } + } + + public String getPath() { + return getString(KEY_PATH); + } + + public void setPath(String path) { + put(KEY_PATH, path); + } + + @SuppressWarnings("unchecked") + public Map getProperties() { + Object properties = get(KEY_PROPERTIES); + return properties != null? (Map)properties : new HashMap(); + } + + public void setProperties(Map properties) { + if (properties != null && !properties.isEmpty()) { + put(KEY_PROPERTIES, properties); + } else { + removeField(KEY_PROPERTIES); + } + } + + public Long getRevisionId() { + return getLong(KEY_REVISION_ID); + } + + public void setRevisionId(long revisionId) { + put(KEY_REVISION_ID, revisionId); + } + + //-------------------------------------------------------------------------- + // + // These properties are used to keep track of changes but not persisted + // + //-------------------------------------------------------------------------- + + public void addChild(String childName) { + if (addedChildren == null) { + addedChildren = new LinkedList(); + } + addedChildren.add(childName); + } + + public List getAddedChildren() { + return addedChildren; + } + + public void removeChild(String childName) { + if (removedChildren == null) { + removedChildren = new LinkedList(); + } + removedChildren.add(childName); + } + + public List getRemovedChildren() { + return removedChildren; + } + + public void addProperty(String key, Object value) { + if (addedProps == null) { + addedProps = new HashMap(); + } + addedProps.put(key, value); + } + + public Map getAddedProps() { + return addedProps; + } + + public void removeProp(String key) { + if (removedProps == null) { + removedProps = new HashMap(); + } + removedProps.put(key, null); + } + + public Map getRemovedProps() { + return removedProps; + } + + //-------------------------------------------------------------------------- + // + // Other methods + // + //-------------------------------------------------------------------------- + + public boolean childExists(String childName) { + List children = getChildren(); + if (children != null && !children.isEmpty()) { + if (children.contains(childName) && !childExistsInRemovedChildren(childName)) { + return true; + } + } + return childExistsInAddedChildren(childName); + } + + private boolean childExistsInAddedChildren(String childName) { + return addedChildren != null && !addedChildren.isEmpty()? + addedChildren.contains(childName) : false; + } + + private boolean childExistsInRemovedChildren(String childName) { + return removedChildren != null && !removedChildren.isEmpty()? + removedChildren.contains(childName) : false; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(super.toString()); + sb.deleteCharAt(sb.length() - 1); + if (addedChildren != null && !addedChildren.isEmpty()) { + sb.append(", addedChildren : "); + sb.append(addedChildren); + } + if (removedChildren != null && !removedChildren.isEmpty()) { + sb.append(", removedChildren : "); + sb.append(removedChildren); + } + if (addedProps != null && !addedProps.isEmpty()) { + sb.append(", addedProps : "); + sb.append(addedProps); + } + if (removedProps != null && !removedProps.isEmpty()) { + sb.append(", removedProps : "); + sb.append(removedProps); + } + sb.append(" }"); + return sb.toString(); + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/model/NotFoundException.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/model/NotFoundException.java new file mode 100644 index 00000000000..da3f82f4097 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/model/NotFoundException.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.model; + +public class NotFoundException extends RuntimeException { + + private static final long serialVersionUID = 267748774351258035L; + + public NotFoundException() { + super(); + } + + public NotFoundException(String message) { + super(message); + } + + public NotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public NotFoundException(Throwable cause) { + super(cause); + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/model/SyncMongo.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/model/SyncMongo.java new file mode 100644 index 00000000000..b74e8313c82 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/model/SyncMongo.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.mongomk.model; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + +/** + * The {@code MongoDB} representation of the head revision. + */ +public class SyncMongo extends BasicDBObject { + + public static final String KEY_HEAD_REVISION_ID = "headRevId"; + public static final String KEY_NEXT_REVISION_ID = "nextRevId"; + private static final long serialVersionUID = 3541425042129003691L; + + public static SyncMongo fromDBObject(DBObject dbObject) { + if (dbObject == null) { + return null; + } + SyncMongo syncMongo = new SyncMongo(); + syncMongo.putAll(dbObject); + return syncMongo; + } + + public long getHeadRevisionId() { + return getLong(KEY_HEAD_REVISION_ID); + } + + public long getNextRevisionId() { + return getLong(KEY_NEXT_REVISION_ID); + } + + public void setHeadRevisionId(long revisionId) { + put(KEY_HEAD_REVISION_ID, revisionId); + } + + public void setNextRevisionId(long revisionId) { + put(KEY_NEXT_REVISION_ID, revisionId); + } +} diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/osgi/MongoMicroKernelService.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/osgi/MongoMicroKernelService.java new file mode 100644 index 00000000000..a1f3572becf --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/osgi/MongoMicroKernelService.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.jackrabbit.mongomk.osgi; + +import java.util.Map; +import java.util.Properties; + +import org.apache.felix.scr.annotations.Activate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.ConfigurationPolicy; +import org.apache.felix.scr.annotations.Property; +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mongomk.impl.BlobStoreMongo; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.impl.MongoMicroKernel; +import org.apache.jackrabbit.mongomk.impl.NodeStoreMongo; +import org.apache.sling.commons.osgi.PropertiesUtil; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.annotations.Deactivate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +@Component(metatype = true, + label = "%oak.mongomk.label", + description = "%oak.mongomk.description", + policy = ConfigurationPolicy.REQUIRE +) +public class MongoMicroKernelService { + + private static final String DEFAULT_HOST = "localhost"; + private static final int DEFAULT_PORT= 27017; + private static final String DEFAULT_DB = "oak"; + + @Property(value = DEFAULT_HOST) + private static final String PROP_HOST = "host"; + + @Property(intValue = DEFAULT_PORT) + private static final String PROP_PORT = "port"; + + @Property(value = DEFAULT_DB) + private static final String PROP_DB = "db"; + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + private ServiceRegistration reg; + private MongoConnection connection; + + @Activate + private void activate(BundleContext context,Map config) throws Exception { + String host = PropertiesUtil.toString(config.get(PROP_HOST), DEFAULT_HOST); + int port = PropertiesUtil.toInteger(config.get(PROP_PORT), DEFAULT_PORT); + String db = PropertiesUtil.toString(config.get(PROP_DB), DEFAULT_DB); + + logger.info("Starting MongoDB MicroKernel with host={}, port={}, db={}", + new Object[] {host, port, db}); + connection = new MongoConnection(host, port, db); + + connection.initializeDB(false); + logger.info("Connected to database {}", db); + + NodeStoreMongo nodeStore = new NodeStoreMongo(connection); + BlobStoreMongo blobStore = new BlobStoreMongo(connection); + MongoMicroKernel mk = new MongoMicroKernel(nodeStore, blobStore); + + Properties props = new Properties(); + props.setProperty("oak.mk.type","mongo"); + reg = context.registerService(MicroKernel.class.getName(),mk,props); + } + + @Deactivate + private void deactivate() { + if (reg != null){ + reg.unregister(); + } + + if (connection != null){ + connection.close(); + } + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/util/MongoUtil.java b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/util/MongoUtil.java new file mode 100644 index 00000000000..9f4de4289a0 --- /dev/null +++ b/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/util/MongoUtil.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.mongomk.util; + +import org.apache.jackrabbit.mk.model.tree.NodeState; +import org.apache.jackrabbit.mongomk.api.model.Node; +import org.apache.jackrabbit.mongomk.impl.model.tree.MongoNodeState; + +/** + * MongoMK specific utility class. + */ +public class MongoUtil { + + public static String fromMongoRepresentation(Long revisionId) { + return String.valueOf(revisionId); + } + + public static Long toMongoRepresentation(String revisionId) throws Exception { + if (revisionId == null) { + return null; + } + try { + return Long.parseLong(revisionId); + } catch (NumberFormatException e) { + throw new Exception("Invalid revision id: " + revisionId); + } + } + + public static NodeState wrap(Node node) { + return node != null? new MongoNodeState(node) : null; + } + + public static String adjustPath(String path) { + return (path == null || path.isEmpty()) ? "/" : path; + } + + public static boolean isFiltered(String path) { + return !"/".equals(path); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/main/resources/OSGI-INF/metatype/metatype.properties b/oak-mongomk/src/main/resources/OSGI-INF/metatype/metatype.properties new file mode 100644 index 00000000000..e172385c919 --- /dev/null +++ b/oak-mongomk/src/main/resources/OSGI-INF/metatype/metatype.properties @@ -0,0 +1,31 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +oak.mongomk.label=Apache Jackrabbit Oak MongoDB MicroKernel Service +oak.mongomk.description= Configure an instance of the MongoDB \ + based MicroKernel implementation + +host.name = MongoDB Host +host.description = The host to connect to. + +port.name = MongoDB Port +port.description = The port to connect to. + +db.name = MongoDB Database +db.description = The database to use. diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/BaseMongoMicroKernelTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/BaseMongoMicroKernelTest.java new file mode 100644 index 00000000000..d408477b8a5 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/BaseMongoMicroKernelTest.java @@ -0,0 +1,191 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk; + +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 org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.blobs.BlobStore; +import org.apache.jackrabbit.mongomk.api.NodeStore; +import org.apache.jackrabbit.mongomk.impl.BlobStoreMongo; +import org.apache.jackrabbit.mongomk.impl.MongoMicroKernel; +import org.apache.jackrabbit.mongomk.impl.NodeStoreMongo; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.junit.Before; + +/** + * Base class for {@code MongoDB} tests that need the MongoMK. + */ +public class BaseMongoMicroKernelTest extends BaseMongoTest { + + public static MicroKernel mk; + + @Before + public void setUp() throws Exception { + super.setUp(); + NodeStore nodeStore = new NodeStoreMongo(mongoConnection); + BlobStore blobStore = new BlobStoreMongo(mongoConnection); + mk = new MongoMicroKernel(nodeStore, blobStore); + } + + protected JSONObject getObjectArrayEntry(JSONArray array, int pos) { + assertTrue(pos >= 0 && pos < array.size()); + Object entry = array.get(pos); + if (entry instanceof JSONObject) { + return (JSONObject) entry; + } + throw new AssertionError("failed to resolve JSONObject array entry at pos " + pos + ": " + entry); + } + + protected JSONArray parseJSONArray(String json) throws AssertionError { + JSONParser parser = new JSONParser(); + try { + Object obj = parser.parse(json); + assertTrue(obj instanceof JSONArray); + return (JSONArray) obj; + } catch (Exception e) { + throw new AssertionError("not a valid JSON array: " + e.getMessage()); + } + } + + protected JSONObject parseJSONObject(String json) throws AssertionError { + JSONParser parser = new JSONParser(); + try { + Object obj = parser.parse(json); + assertTrue(obj instanceof JSONObject); + return (JSONObject) obj; + } catch (Exception e) { + throw new AssertionError("not a valid JSON object: " + e.getMessage()); + } + } + + protected void assertNodesExist(String revision, String...paths) { + doAssertNodes(true, revision, paths); + } + + protected void assertNodesNotExist(String revision, String...paths) { + doAssertNodes(false, revision, paths); + } + + protected void assertPropExists(String rev, String path, String property) { + String nodes = mk.getNodes(path, rev, -1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyExists(obj, property); + } + + protected void assertPropNotExists(String rev, String path, String property) { + String nodes = mk.getNodes(path, rev, -1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + if (nodes == null) { + return; + } + JSONObject obj = parseJSONObject(nodes); + assertPropertyNotExists(obj, property); + } + + protected void assertPropValue(String rev, String path, String property, String value) { + String nodes = mk.getNodes(path, rev, -1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyValue(obj, property, value); + } + + protected void assertPropertyExists(JSONObject obj, String relPath, Class type) + throws AssertionError { + Object val = resolveValue(obj, relPath); + assertNotNull("not found: " + relPath, val); + + assertTrue(type.isInstance(val)); + } + + protected void assertPropertyExists(JSONObject obj, String relPath) + throws AssertionError { + Object val = resolveValue(obj, relPath); + assertNotNull("not found: " + relPath, val); + } + + protected void assertPropertyNotExists(JSONObject obj, String relPath) + throws AssertionError { + Object val = resolveValue(obj, relPath); + assertNull(val); + } + + protected void assertPropertyValue(JSONObject obj, String relPath, String expected) + throws AssertionError { + Object val = resolveValue(obj, relPath); + assertNotNull("not found: " + relPath, val); + assertEquals(expected, val); + } + + protected void assertPropertyValue(JSONObject obj, String relPath, Double expected) + throws AssertionError { + Object val = resolveValue(obj, relPath); + assertNotNull("not found: " + relPath, val); + + assertEquals(expected, val); + } + + protected void assertPropertyValue(JSONObject obj, String relPath, Long expected) + throws AssertionError { + Object val = resolveValue(obj, relPath); + assertNotNull("not found: " + relPath, val); + assertEquals(expected, val); + } + + protected void assertPropertyValue(JSONObject obj, String relPath, Boolean expected) + throws AssertionError { + Object val = resolveValue(obj, relPath); + assertNotNull("not found: " + relPath, val); + + assertEquals(expected, val); + } + + private void doAssertNodes(boolean checkExists, String revision, String...paths) { + for (String path : paths) { + boolean exists = mk.nodeExists(path, revision); + if (checkExists) { + assertTrue(path + " does not exist", exists); + } else { + assertFalse(path + " should not exist", exists); + } + } + } + + protected JSONObject resolveObjectValue(JSONObject obj, String relPath) { + Object val = resolveValue(obj, relPath); + if (val instanceof JSONObject) { + return (JSONObject) val; + } + throw new AssertionError("failed to resolve JSONObject value at " + relPath + ": " + val); + } + + private Object resolveValue(JSONObject obj, String relPath) { + String names[] = relPath.split("/"); + Object val = obj; + for (String name : names) { + if (! (val instanceof JSONObject)) { + throw new AssertionError("not found: " + relPath); + } + val = ((JSONObject) val).get(name); + } + return val; + } +} \ No newline at end of file diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/BaseMongoTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/BaseMongoTest.java new file mode 100644 index 00000000000..7a5821e5834 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/BaseMongoTest.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.mongomk; + +import java.io.InputStream; +import java.util.Properties; + +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; + +/** + * Base class for {@code MongoDB} tests that only need a Mongo connection rather + * than the full MongoMK. + */ +public class BaseMongoTest { + + public static MongoConnection mongoConnection; + + @BeforeClass + public static void setUpBeforeClass() throws Exception { + createDefaultMongoConnection(); + MongoAssert.setMongoConnection(mongoConnection); + } + + @AfterClass + public static void tearDownAfterClass() throws Exception { + mongoConnection.getDB().dropDatabase(); + } + + private static void createDefaultMongoConnection() throws Exception { + InputStream is = BaseMongoTest.class.getResourceAsStream("/config.cfg"); + Properties properties = new Properties(); + properties.load(is); + + String host = properties.getProperty("host"); + int port = Integer.parseInt(properties.getProperty("port")); + String database = properties.getProperty("db"); + + mongoConnection = new MongoConnection(host, port, database); + } + + @Before + public void setUp() throws Exception { + mongoConnection.initializeDB(true); + } + + @After + public void tearDown() throws Exception { + mongoConnection.clearDB(); + } +} diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/MongoAssert.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/MongoAssert.java new file mode 100644 index 00000000000..70b25889bc1 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/MongoAssert.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.jackrabbit.mongomk.api.model.Commit; +import org.apache.jackrabbit.mongomk.api.model.Node; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.model.CommitMongo; +import org.apache.jackrabbit.mongomk.model.SyncMongo; +import org.apache.jackrabbit.mongomk.model.NodeMongo; +import org.apache.jackrabbit.mongomk.util.MongoUtil; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.junit.Assert; + +import com.mongodb.DBCollection; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; + +/** + * Assertion utilities for {@code MongoDB} tests. + */ +public class MongoAssert { + + private static MongoConnection mongoConnection; + + public static void assertCommitContainsAffectedPaths(String revisionId, + String... expectedPaths) throws Exception { + DBCollection commitCollection = mongoConnection.getCommitCollection(); + DBObject query = QueryBuilder.start(CommitMongo.KEY_REVISION_ID) + .is(MongoUtil.toMongoRepresentation(revisionId)).get(); + CommitMongo result = (CommitMongo) commitCollection.findOne(query); + Assert.assertNotNull(result); + + List actualPaths = result.getAffectedPaths(); + Assert.assertEquals(new HashSet(Arrays.asList(expectedPaths)), new HashSet(actualPaths)); + } + + public static void assertCommitExists(Commit commit) { + DBCollection commitCollection = mongoConnection.getCommitCollection(); + DBObject query = QueryBuilder.start(CommitMongo.KEY_REVISION_ID) + .is(commit.getRevisionId()).and(CommitMongo.KEY_MESSAGE) + .is(commit.getMessage()).and(CommitMongo.KEY_DIFF).is(commit.getDiff()).and(CommitMongo.KEY_PATH) + .is(commit.getPath()).and(CommitMongo.KEY_FAILED).notEquals(Boolean.TRUE).get(); + CommitMongo result = (CommitMongo) commitCollection.findOne(query); + Assert.assertNotNull(result); + } + + public static void assertHeadRevision(long revisionId) { + DBCollection headCollection = mongoConnection.getSyncCollection(); + SyncMongo result = (SyncMongo) headCollection.findOne(); + Assert.assertEquals(revisionId, result.getHeadRevisionId()); + } + + public static void assertNextRevision(long revisionId) { + DBCollection headCollection = mongoConnection.getSyncCollection(); + SyncMongo result = (SyncMongo) headCollection.findOne(); + Assert.assertEquals(revisionId, result.getNextRevisionId()); + } + + public static void assertNodeRevisionId(String path, String revisionId, + boolean exists) throws Exception { + DBCollection nodeCollection = mongoConnection.getNodeCollection(); + DBObject query = QueryBuilder.start(NodeMongo.KEY_PATH).is(path).and(NodeMongo.KEY_REVISION_ID) + .is(MongoUtil.toMongoRepresentation(revisionId)).get(); + NodeMongo nodeMongo = (NodeMongo) nodeCollection.findOne(query); + + if (exists) { + Assert.assertNotNull(nodeMongo); + } else { + Assert.assertNull(nodeMongo); + } + } + + public static void assertNodesExist(Node expected) { + DBCollection nodeCollection = mongoConnection.getNodeCollection(); + QueryBuilder qb = QueryBuilder.start(NodeMongo.KEY_PATH).is(expected.getPath()) + .and(NodeMongo.KEY_REVISION_ID) + .is(expected.getRevisionId()); + Map properties = expected.getProperties(); + if (properties != null) { + for (Map.Entry entry : properties.entrySet()) { + qb.and(NodeMongo.KEY_PROPERTIES + "." + entry.getKey()).is(entry.getValue()); + } + } + + DBObject query = qb.get(); + + NodeMongo nodeMongo = (NodeMongo) nodeCollection.findOne(query); + Assert.assertNotNull(nodeMongo); + + List nodeMongoChildren = nodeMongo.getChildren(); + int actual = nodeMongoChildren != null? nodeMongoChildren.size() : 0; + Assert.assertEquals(expected.getChildNodeCount(), actual); + + for (Iterator it = expected.getChildNodeEntries(0, -1); it.hasNext(); ) { + Node childNode = it.next(); + assertNodesExist(childNode); + String childName = PathUtils.getName(childNode.getPath()); + Assert.assertTrue(nodeMongoChildren.contains(childName)); + } + } + + static void setMongoConnection(MongoConnection mongoConnection) { + // must be set prior to using this class. + MongoAssert.mongoConnection = mongoConnection; + } + + private MongoAssert() { + // no instantiation + } +} diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/action/FetchNodesActionTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/action/FetchNodesActionTest.java new file mode 100644 index 00000000000..709c0fd588c --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/action/FetchNodesActionTest.java @@ -0,0 +1,267 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.action; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.apache.jackrabbit.mongomk.BaseMongoTest; +import org.apache.jackrabbit.mongomk.action.FetchNodesAction; +import org.apache.jackrabbit.mongomk.api.model.Commit; +import org.apache.jackrabbit.mongomk.api.model.Node; +import org.apache.jackrabbit.mongomk.impl.NodeAssert; +import org.apache.jackrabbit.mongomk.impl.builder.NodeBuilder; +import org.apache.jackrabbit.mongomk.impl.command.CommitCommand; +import org.apache.jackrabbit.mongomk.impl.model.CommitBuilder; +import org.apache.jackrabbit.mongomk.model.CommitMongo; +import org.apache.jackrabbit.mongomk.model.NodeMongo; +import org.junit.Test; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBCollection; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; + +public class FetchNodesActionTest extends BaseMongoTest { + + @Test + public void invalidFirstRevision() throws Exception { + Long revisionId1 = addNode("a"); + Long revisionId2 = addNode("b"); + Long revisionId3 = addNode("c"); + + invalidateCommit(revisionId1); + updateBaseRevisionId(revisionId2, 0L); + + FetchNodesAction query = new FetchNodesAction(mongoConnection, + "/", true, revisionId3); + List actuals = NodeMongo.toNode(query.execute()); + + String json = String.format("{\"/#%2$s\" : { \"b#%1$s\" : {}, \"c#%2$s\" : {} }}", + revisionId2, revisionId3); + Iterator expecteds = NodeBuilder.build(json).getChildNodeEntries(0, -1); + NodeAssert.assertEquals(expecteds, actuals); + } + + @Test + public void invalidLastRevision() throws Exception { + Long revisionId1 = addNode("a"); + Long revisionId2 = addNode("b"); + Long revisionId3 = addNode("c"); + + invalidateCommit(revisionId3); + + FetchNodesAction query = new FetchNodesAction(mongoConnection, + "/", true, revisionId3); + List actuals = NodeMongo.toNode(query.execute()); + + String json = String.format("{\"/#%2$s\" : { \"a#%1$s\" : {}, \"b#%2$s\" : {} }}", + revisionId1, revisionId2); + Iterator expecteds = NodeBuilder.build(json).getChildNodeEntries(0, -1); + NodeAssert.assertEquals(expecteds, actuals); + } + + @Test + public void invalidMiddleRevision() throws Exception { + Long revisionId1 = addNode("a"); + Long revisionId2 = addNode("b"); + Long revisionId3 = addNode("c"); + + invalidateCommit(revisionId2); + updateBaseRevisionId(revisionId3, revisionId1); + + FetchNodesAction query = new FetchNodesAction(mongoConnection, + "/", true, revisionId3); + List actuals = NodeMongo.toNode(query.execute()); + + String json = String.format("{\"/#%2$s\" : { \"a#%1$s\" : {}, \"c#%2$s\" : {} }}", + revisionId1, revisionId3); + Iterator expecteds = NodeBuilder.build(json).getChildNodeEntries(0, -1); + NodeAssert.assertEquals(expecteds, actuals); + } + + // FIXME - Revisit this test. + @Test + public void fetchRootAndAllDepths() throws Exception { + SimpleNodeScenario scenario = new SimpleNodeScenario(mongoConnection); + Long firstRevisionId = scenario.create(); + Long secondRevisionId = scenario.update_A_and_add_D_and_E(); + + FetchNodesAction query = new FetchNodesAction(mongoConnection, + "/", true, firstRevisionId); + query.setDepth(0); + List result = query.execute(); + List actuals = NodeMongo.toNode(result); + String json = String.format("{ \"/#%1$s\" : {} }", firstRevisionId); + Node expected = NodeBuilder.build(json); + Iterator expecteds = expected.getChildNodeEntries(0, -1); + NodeAssert.assertEquals(expecteds, actuals); + + query = new FetchNodesAction(mongoConnection, "/", true, secondRevisionId); + query.setDepth(0); + result = query.execute(); + actuals = NodeMongo.toNode(result); + json = String.format("{ \"/#%1$s\" : {} }", firstRevisionId); + expected = NodeBuilder.build(json); + expecteds = expected.getChildNodeEntries(0, -1); + NodeAssert.assertEquals(expecteds, actuals); + + query = new FetchNodesAction(mongoConnection, "/", true, firstRevisionId); + query.setDepth(1); + result = query.execute(); + actuals = NodeMongo.toNode(result); + json = String.format("{ \"/#%1$s\" : { \"a#%1$s\" : { \"int\" : 1 } } }", firstRevisionId); + expected = NodeBuilder.build(json); + expecteds = expected.getChildNodeEntries(0, -1); + NodeAssert.assertEquals(expecteds, actuals); + + query = new FetchNodesAction(mongoConnection, "/", true, secondRevisionId); + query.setDepth(1); + result = query.execute(); + actuals = NodeMongo.toNode(result); + json = String.format("{ \"/#%1$s\" : { \"a#%2$s\" : { \"int\" : 1 , \"double\" : 0.123 } } }", + firstRevisionId, secondRevisionId); + expected = NodeBuilder.build(json); + expecteds = expected.getChildNodeEntries(0, -1); + NodeAssert.assertEquals(expecteds, actuals); + + query = new FetchNodesAction(mongoConnection, "/", true, firstRevisionId); + query.setDepth(2); + result = query.execute(); + actuals = NodeMongo.toNode(result); + json = String.format("{ \"/#%1$s\" : { \"a#%1$s\" : { \"int\" : 1, \"b#%1$s\" : { \"string\" : \"foo\" } , \"c#%1$s\" : { \"bool\" : true } } } }", + firstRevisionId); + expected = NodeBuilder.build(json); + expecteds = expected.getChildNodeEntries(0, -1); + NodeAssert.assertEquals(expecteds, actuals); + + query = new FetchNodesAction(mongoConnection, "/", true, secondRevisionId); + query.setDepth(2); + result = query.execute(); + actuals = NodeMongo.toNode(result); + json = String.format("{ \"/#%1$s\" : { \"a#%2$s\" : { \"int\" : 1 , \"double\" : 0.123 , \"b#%2$s\" : { \"string\" : \"foo\" } , \"c#%1$s\" : { \"bool\" : true }, \"d#%2$s\" : { \"null\" : null } } } }", + firstRevisionId, secondRevisionId); + expected = NodeBuilder.build(json); + expecteds = expected.getChildNodeEntries(0, -1); + NodeAssert.assertEquals(expecteds, actuals); + + query = new FetchNodesAction(mongoConnection, "/", true, firstRevisionId); + result = query.execute(); + actuals = NodeMongo.toNode(result); + json = String.format("{ \"/#%1$s\" : { \"a#%1$s\" : { \"int\" : 1 , \"b#%1$s\" : { \"string\" : \"foo\" } , \"c#%1$s\" : { \"bool\" : true } } } }", + firstRevisionId); + expected = NodeBuilder.build(json); + expecteds = expected.getChildNodeEntries(0, -1); + NodeAssert.assertEquals(expecteds, actuals); + + query = new FetchNodesAction(mongoConnection, "/", true, secondRevisionId); + result = query.execute(); + actuals = NodeMongo.toNode(result); + json = String.format("{ \"/#%1$s\" : { \"a#%2$s\" : { \"int\" : 1 , \"double\" : 0.123 , \"b#%2$s\" : { \"string\" : \"foo\", \"e#%2$s\" : { \"array\" : [ 123, null, 123.456, \"for:bar\", true ] } } , \"c#%1$s\" : { \"bool\" : true }, \"d#%2$s\" : { \"null\" : null } } } }", + firstRevisionId, secondRevisionId); + expected = NodeBuilder.build(json); + expecteds = expected.getChildNodeEntries(0, -1); + NodeAssert.assertEquals(expecteds, actuals); + } + + private Long addNode(String nodeName) throws Exception { + Commit commit = CommitBuilder.build("/", "+\"" + nodeName + "\" : {}", "Add /" + nodeName); + CommitCommand command = new CommitCommand(mongoConnection, commit); + return command.execute(); + } + + @Test + public void fetchWithCertainPathsOneRevision() throws Exception { + SimpleNodeScenario scenario = new SimpleNodeScenario(mongoConnection); + Long revisionId = scenario.create(); + + FetchNodesAction query = new FetchNodesAction(mongoConnection, + getPathSet("/a", "/a/b", "/a/c", "not_existing"), revisionId); + List nodeMongos = query.execute(); + List actuals = NodeMongo.toNode(nodeMongos); + String json = String.format("{ \"/#%1$s\" : { \"a#%1$s\" : { \"int\" : 1 , \"b#%1$s\" : { \"string\" : \"foo\" } , \"c#%1$s\" : { \"bool\" : true } } } }", + revisionId); + Node expected = NodeBuilder.build(json); + Iterator expecteds = expected.getChildNodeEntries(0, -1); + NodeAssert.assertEquals(expecteds, actuals); + + query = new FetchNodesAction(mongoConnection, + getPathSet("/a", "not_existing"), revisionId); + nodeMongos = query.execute(); + actuals = NodeMongo.toNode(nodeMongos); + json = String.format("{ \"/#%1$s\" : { \"a#%1$s\" : { \"int\" : 1 } } }", + revisionId); + expected = NodeBuilder.build(json); + expecteds = expected.getChildNodeEntries(0, -1); + NodeAssert.assertEquals(expecteds, actuals); + } + + @Test + public void fetchWithCertainPathsTwoRevisions() throws Exception { + SimpleNodeScenario scenario = new SimpleNodeScenario(mongoConnection); + Long firstRevisionId = scenario.create(); + Long secondRevisionId = scenario.update_A_and_add_D_and_E(); + + FetchNodesAction query = new FetchNodesAction(mongoConnection, + getPathSet("/a", "/a/b", "/a/c", "/a/d", "/a/b/e", "not_existing"), + firstRevisionId); + List nodeMongos = query.execute(); + List actuals = NodeMongo.toNode(nodeMongos); + String json = String.format("{ \"/#%1$s\" : { \"a#%1$s\" : { \"int\" : 1 , \"b#%1$s\" : { \"string\" : \"foo\" } , \"c#%1$s\" : { \"bool\" : true } } } }", + firstRevisionId); + Node expected = NodeBuilder.build(json); + Iterator expecteds = expected.getChildNodeEntries(0, -1); + NodeAssert.assertEquals(expecteds, actuals); + + query = new FetchNodesAction(mongoConnection, + getPathSet("/a", "/a/b", "/a/c", "/a/d", "/a/b/e", "not_existing"), + secondRevisionId); + nodeMongos = query.execute(); + actuals = NodeMongo.toNode(nodeMongos); + json = String.format("{ \"/#%1$s\" : { \"a#%2$s\" : { \"int\" : 1 , \"double\" : 0.123 , \"b#%2$s\" : { \"string\" : \"foo\" , \"e#%2$s\" : { \"array\" : [ 123, null, 123.456, \"for:bar\", true ] } } , \"c#%1$s\" : { \"bool\" : true }, \"d#%2$s\" : { \"null\" : null } } } }", + firstRevisionId, secondRevisionId); + expected = NodeBuilder.build(json); + expecteds = expected.getChildNodeEntries(0, -1); + NodeAssert.assertEquals(expecteds, actuals); + } + + private Set getPathSet(String... paths) { + return new HashSet(Arrays.asList(paths)); + } + + private void invalidateCommit(Long revisionId) { + DBCollection commitCollection = mongoConnection.getCommitCollection(); + DBObject query = QueryBuilder.start(CommitMongo.KEY_REVISION_ID) + .is(revisionId).get(); + DBObject update = new BasicDBObject(); + update.put("$set", new BasicDBObject(CommitMongo.KEY_FAILED, Boolean.TRUE)); + commitCollection.update(query, update); + } + + private void updateBaseRevisionId(Long revisionId2, Long baseRevisionId) { + DBCollection commitCollection = mongoConnection.getCommitCollection(); + DBObject query = QueryBuilder.start(CommitMongo.KEY_REVISION_ID) + .is(revisionId2) + .get(); + DBObject update = new BasicDBObject("$set", + new BasicDBObject(CommitMongo.KEY_BASE_REVISION_ID, baseRevisionId)); + commitCollection.update(query, update); + } +} diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/action/FetchValidCommitsActionTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/action/FetchValidCommitsActionTest.java new file mode 100644 index 00000000000..9aa930c0784 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/action/FetchValidCommitsActionTest.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.action; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.apache.jackrabbit.mongomk.BaseMongoTest; +import org.apache.jackrabbit.mongomk.action.FetchCommitsAction; +import org.apache.jackrabbit.mongomk.model.CommitMongo; +import org.junit.Test; + +public class FetchValidCommitsActionTest extends BaseMongoTest { + + private static final int MIN_COMMITS = 1; + private static final int SIMPLE_SCENARIO_COMMITS = MIN_COMMITS + 1; + + @Test + public void simple() throws Exception { + FetchCommitsAction action = new FetchCommitsAction(mongoConnection); + List commits = action.execute(); + assertEquals(MIN_COMMITS, commits.size()); + + SimpleNodeScenario scenario = new SimpleNodeScenario(mongoConnection); + scenario.create(); + commits = action.execute(); + assertEquals(SIMPLE_SCENARIO_COMMITS, commits.size()); + + int numberOfChildren = 3; + scenario.addChildrenToA(numberOfChildren); + commits = action.execute(); + assertEquals(SIMPLE_SCENARIO_COMMITS + numberOfChildren, commits.size()); + } + + @Test + public void revisionId() throws Exception { + FetchCommitsAction action = new FetchCommitsAction(mongoConnection); + List commits = action.execute(); + CommitMongo commit0 = commits.get(0); + + SimpleNodeScenario scenario = new SimpleNodeScenario(mongoConnection); + scenario.create(); + commits = action.execute(); + CommitMongo commit1 = commits.get(0); + assertTrue(commit0.getRevisionId() < commit1.getRevisionId()); + + int numberOfChildren = 3; + scenario.addChildrenToA(numberOfChildren); + commits = action.execute(); + CommitMongo commit2 = commits.get(0); + assertTrue(commit1.getRevisionId() < commit2.getRevisionId()); + } + + @Test + public void time() throws Exception { + FetchCommitsAction action = new FetchCommitsAction(mongoConnection); + List commits = action.execute(); + CommitMongo commit0 = commits.get(0); + + Thread.sleep(1000); + + SimpleNodeScenario scenario = new SimpleNodeScenario(mongoConnection); + scenario.create(); + commits = action.execute(); + CommitMongo commit1 = commits.get(0); + assertTrue(commit0.getTimestamp() < commit1.getTimestamp()); + + Thread.sleep(1000); + + int numberOfChildren = 3; + scenario.addChildrenToA(numberOfChildren); + commits = action.execute(); + CommitMongo commit2 = commits.get(0); + assertTrue(commit1.getTimestamp() < commit2.getTimestamp()); + } + + @Test + public void maxEntriesDefaultLimitless() throws Exception { + SimpleNodeScenario scenario = new SimpleNodeScenario(mongoConnection); + scenario.create(); + + int numberOfChildren = 2; + scenario.addChildrenToA(numberOfChildren); + + FetchCommitsAction query = new FetchCommitsAction(mongoConnection, + 0L, Long.MAX_VALUE); + List commits = query.execute(); + assertEquals(SIMPLE_SCENARIO_COMMITS + numberOfChildren, commits.size()); + } + + @Test + public void maxEntries() throws Exception { + SimpleNodeScenario scenario = new SimpleNodeScenario(mongoConnection); + scenario.create(); + + int numberOfChildren = 2; + scenario.addChildrenToA(numberOfChildren); + + int maxEntries = 2; + FetchCommitsAction query = new FetchCommitsAction(mongoConnection, + 0L, Long.MAX_VALUE); + query.setMaxEntries(maxEntries); + List commits = query.execute(); + assertEquals(maxEntries, commits.size()); + } +} diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/action/SimpleNodeScenario.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/action/SimpleNodeScenario.java new file mode 100644 index 00000000000..3aa98f91288 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/action/SimpleNodeScenario.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.mongomk.action; + +import org.apache.jackrabbit.mongomk.api.model.Commit; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.impl.command.CommitCommand; +import org.apache.jackrabbit.mongomk.impl.model.CommitBuilder; + +/** + * Creates a defined scenario in {@code MongoDB}. + */ +public class SimpleNodeScenario { + + private final MongoConnection mongoConnection; + + /** + * Constructs a new {@code SimpleNodeScenario}. + * + * @param mongoConnection The {@link MongoConnection}. + */ + public SimpleNodeScenario(MongoConnection mongoConnection) { + this.mongoConnection = mongoConnection; + } + + /** + * Creates the following nodes: + * + *
    +     * "+a : { \"int\" : 1 , \"b\" : { \"string\" : \"foo\" } , \"c\" : { \"bool\" : true } } }"
    +     * 
    + * + * @return The {@link RevisionId}. + * @throws Exception If an error occurred. + */ + public Long create() throws Exception { + Commit commit = CommitBuilder.build("/", + "+\"a\" : { \"int\" : 1 , \"b\" : { \"string\" : \"foo\" } , \"c\" : { \"bool\" : true } }", + "This is the simple node scenario with nodes /, /a, /a/b, /a/c"); + CommitCommand command = new CommitCommand(mongoConnection, commit); + return command.execute(); + } + + public Long addChildrenToA(int count) throws Exception { + Long revisionId = null; + for (int i = 1; i <= count; i++) { + Commit commit = CommitBuilder.build("/a", "+\"child" + i + "\" : {}", "Add child" + i); + CommitCommand command = new CommitCommand(mongoConnection, commit); + revisionId = command.execute(); + } + return revisionId; + } + + public Long delete_A() throws Exception { + Commit commit = CommitBuilder.build("/", "-\"a\"", "This is a commit with deleted /a"); + CommitCommand command = new CommitCommand(mongoConnection, commit); + return command.execute(); + } + + public Long delete_B() throws Exception { + Commit commit = CommitBuilder.build("/a", "-\"b\"", "This is a commit with deleted /a/b"); + CommitCommand command = new CommitCommand(mongoConnection, commit); + return command.execute(); + } + + public Long update_A_and_add_D_and_E() throws Exception { + StringBuilder diff = new StringBuilder(); + diff.append("+\"a/d\" : {}"); + diff.append("+\"a/b/e\" : {}"); + diff.append("^\"a/double\" : 0.123"); + diff.append("^\"a/d/int\" : 2"); + diff.append("^\"a/b/e/array\" : [ 123, null, 123.456, \"for:bar\", true ]"); + Commit commit = CommitBuilder.build("/", diff.toString(), + "This is a commit with updated /a and added /a/d and /a/b/e"); + CommitCommand command = new CommitCommand(mongoConnection, commit); + return command.execute(); + } +} diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/InstructionAssert.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/InstructionAssert.java new file mode 100644 index 00000000000..2210eb223ce --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/InstructionAssert.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.mongomk.impl; + +import static junit.framework.Assert.assertEquals; + +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.AddNodeInstruction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.CopyNodeInstruction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.MoveNodeInstruction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.RemoveNodeInstruction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.SetPropertyInstruction; + +public class InstructionAssert { + + public static void assertAddNodeInstruction(AddNodeInstruction instruction, String path) { + assertEquals(path, instruction.getPath()); + } + + public static void assertCopyNodeInstruction(CopyNodeInstruction instruction, String path, String sourcePath, + String destPath) { + assertEquals(path, instruction.getPath()); + assertEquals(sourcePath, instruction.getSourcePath()); + assertEquals(destPath, instruction.getDestPath()); + } + + public static void assertMoveNodeInstruction(MoveNodeInstruction instruction, String parentPath, String oldPath, + String newPath) { + assertEquals(parentPath, instruction.getPath()); + assertEquals(oldPath, instruction.getSourcePath()); + assertEquals(newPath, instruction.getDestPath()); + } + + public static void assertRemoveNodeInstruction(RemoveNodeInstruction instruction, String path) { + assertEquals(path, instruction.getPath()); + } + + public static void assertSetPropertyInstruction(SetPropertyInstruction instruction, String path, String key, + Object value) { + assertEquals(path, instruction.getPath()); + assertEquals(key, instruction.getKey()); + assertEquals(value, instruction.getValue()); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKBranchMergeTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKBranchMergeTest.java new file mode 100644 index 00000000000..367c323c387 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKBranchMergeTest.java @@ -0,0 +1,331 @@ +package org.apache.jackrabbit.mongomk.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.apache.jackrabbit.mongomk.BaseMongoMicroKernelTest; +import org.junit.Test; + +/** + * Tests for {@code MicroKernel#branch} + */ +public class MongoMKBranchMergeTest extends BaseMongoMicroKernelTest { + + @Test + public void oneBranchAddedChildren1() { + addNodes(null, "/trunk", "/trunk/child1"); + assertNodesExist(null, "/trunk", "/trunk/child1"); + + String branchRev = mk.branch(null); + + branchRev = addNodes(branchRev, "/branch1", "/branch1/child1"); + assertNodesExist(branchRev, "/trunk", "/trunk/child1"); + assertNodesExist(branchRev, "/branch1", "/branch1/child1"); + assertNodesNotExist(null, "/branch1", "/branch1/child1"); + + addNodes(null, "/trunk/child2"); + assertNodesExist(null, "/trunk/child2"); + assertNodesNotExist(branchRev, "/trunk/child2"); + + mk.merge(branchRev, ""); + assertNodesExist(null, "/trunk", "/trunk/child1", "/trunk/child2", "/branch1", "/branch1/child1"); + } + + @Test + public void oneBranchAddedChildren2() { + addNodes(null, "/trunk", "/trunk/child1"); + assertNodesExist(null, "/trunk", "/trunk/child1"); + + String branchRev = mk.branch(null); + + branchRev = addNodes(branchRev, "/trunk/child1/child2"); + assertNodesExist(branchRev, "/trunk", "/trunk/child1"); + assertNodesExist(branchRev, "/trunk/child1/child2"); + assertNodesNotExist(null, "/trunk/child1/child2"); + + addNodes(null, "/trunk/child3"); + assertNodesExist(null, "/trunk/child3"); + assertNodesNotExist(branchRev, "/trunk/child3"); + + mk.merge(branchRev, ""); + assertNodesExist(null, "/trunk", "/trunk/child1", "/trunk/child1/child2", "/trunk/child3"); + } + + @Test + public void oneBranchAddedChildren3() { + addNodes(null, "/root", "/root/child1"); + assertNodesExist(null, "/root", "/root/child1"); + + String branchRev = mk.branch(null); + + addNodes(null, "/root/child2"); + assertNodesExist(null, "/root", "/root/child1", "/root/child2"); + assertNodesExist(branchRev, "/root", "/root/child1"); + assertNodesNotExist(branchRev, "/root/child2"); + + branchRev = addNodes(branchRev, "/root/child1/child3", "/root/child4"); + assertNodesExist(branchRev, "/root", "/root/child1", "/root/child1/child3", "/root/child4"); + assertNodesNotExist(branchRev, "/root/child2"); + assertNodesExist(null, "/root", "/root/child1", "/root/child2"); + assertNodesNotExist(null, "/root/child1/child3", "/root/child4"); + + mk.merge(branchRev, ""); + assertNodesExist(null, "/root", "/root/child1", "/root/child2", + "/root/child1/child3", "/root/child4"); + } + + @Test + public void oneBranchRemovedChildren() { + addNodes(null, "/trunk", "/trunk/child1"); + assertNodesExist(null, "/trunk", "/trunk/child1"); + + String branchRev = mk.branch(null); + + branchRev = removeNodes(branchRev, "/trunk/child1"); + assertNodesExist(branchRev, "/trunk"); + assertNodesNotExist(branchRev, "/trunk/child1"); + assertNodesExist(null, "/trunk", "/trunk/child1"); + + mk.merge(branchRev, ""); + assertNodesExist(null, "/trunk"); + assertNodesNotExist(null, "/trunk/child1"); + } + + @Test + public void oneBranchRemovedRoot() { + addNodes(null, "/trunk", "/trunk/child1"); + assertNodesExist(null, "/trunk", "/trunk/child1"); + + String branchRev = mk.branch(null); + + branchRev = removeNodes(branchRev, "/trunk"); + assertNodesNotExist(branchRev, "/trunk", "/trunk/child1"); + assertNodesExist(null, "/trunk", "/trunk/child1"); + + mk.merge(branchRev, ""); + assertNodesNotExist(null, "/trunk", "/trunk/child1"); + } + + @Test + public void oneBranchChangedProperties() { + addNodes(null, "/trunk", "/trunk/child1"); + setProp(null, "/trunk/child1/prop1", "value1"); + setProp(null, "/trunk/child1/prop2", "value2"); + assertNodesExist(null, "/trunk", "/trunk/child1"); + assertPropExists(null, "/trunk/child1", "prop1"); + assertPropExists(null, "/trunk/child1", "prop2"); + + String branchRev = mk.branch(null); + + branchRev = setProp(branchRev, "/trunk/child1/prop1", "value1a"); + branchRev = setProp(branchRev, "/trunk/child1/prop2", null); + branchRev = setProp(branchRev, "/trunk/child1/prop3", "value3"); + assertPropValue(branchRev, "/trunk/child1", "prop1", "value1a"); + assertPropNotExists(branchRev, "/trunk/child1", "prop2"); + assertPropValue(branchRev, "/trunk/child1", "prop3", "value3"); + assertPropValue(null, "/trunk/child1", "prop1", "value1"); + assertPropExists(null, "/trunk/child1", "prop2"); + assertPropNotExists(null, "/trunk/child1", "prop3"); + + mk.merge(branchRev, ""); + assertPropValue(null, "/trunk/child1", "prop1", "value1a"); + assertPropNotExists(null, "/trunk/child1", "prop2"); + assertPropValue(null, "/trunk/child1", "prop3", "value3"); + } + + @Test + public void oneBranchAddedSubChildren() { + addNodes(null, "/trunk", "/trunk/child1", "/trunk/child1/child2", "/trunk/child1/child2/child3"); + assertNodesExist(null, "/trunk", "/trunk/child1", "/trunk/child1/child2", "/trunk/child1/child2/child3"); + + String branchRev = mk.branch(null); + + branchRev = addNodes(branchRev, "/branch1", "/branch1/child1", "/branch1/child1/child2", "/branch1/child1/child2/child3"); + assertNodesExist(branchRev, "/trunk", "/trunk/child1", "/trunk/child1/child2", "/trunk/child1/child2/child3"); + assertNodesExist(branchRev, "/branch1", "/branch1/child1", "/branch1/child1/child2", "/branch1/child1/child2/child3"); + assertNodesNotExist(null, "/branch1", "/branch1/child1", "/branch1/child1/child2", "/branch1/child1/child2/child3"); + + addNodes(null, "/trunk/child1/child2/child3/child4", "/trunk/child5"); + assertNodesExist(null, "/trunk/child1/child2/child3/child4", "/trunk/child5"); + assertNodesNotExist(branchRev, "/trunk/child1/child2/child3/child4", "/trunk/child5"); + + mk.merge(branchRev, ""); + assertNodesExist(null, "/trunk", "/trunk/child1", "/trunk/child1/child2", "/trunk/child1/child2/child3", "/trunk/child1/child2/child3/child4"); + assertNodesExist(null, "/branch1", "/branch1/child1", "/branch1/child1/child2", "/branch1/child1/child2/child3"); + } + + @Test + public void oneBranchAddedChildrenAndAddedProperties() { + addNodes(null, "/trunk", "/trunk/child1"); + setProp(null, "/trunk/child1/prop1", "value1"); + setProp(null, "/trunk/child1/prop2", "value2"); + assertNodesExist(null, "/trunk", "/trunk/child1"); + assertPropExists(null, "/trunk/child1", "prop1"); + assertPropExists(null, "/trunk/child1", "prop2"); + + String branchRev = mk.branch(null); + + branchRev = addNodes(branchRev, "/branch1", "/branch1/child1"); + branchRev = setProp(branchRev, "/branch1/child1/prop1", "value1"); + branchRev = setProp(branchRev, "/branch1/child1/prop2", "value2"); + assertNodesExist(branchRev, "/trunk", "/trunk/child1"); + assertPropExists(branchRev, "/trunk/child1", "prop1"); + assertPropExists(branchRev, "/trunk/child1", "prop2"); + assertNodesExist(branchRev, "/branch1", "/branch1/child1"); + assertPropExists(branchRev, "/branch1/child1", "prop1"); + assertPropExists(branchRev, "/branch1/child1", "prop2"); + assertNodesNotExist(null, "/branch1", "/branch1/child1"); + assertPropNotExists(null, "/branch1/child1", "prop1"); + assertPropNotExists(null, "/branch1/child1", "prop2"); + + mk.merge(branchRev, ""); + assertNodesExist(null, "/trunk", "/trunk/child1"); + assertPropExists(null, "/trunk/child1", "prop1"); + assertPropExists(null, "/trunk/child1", "prop2"); + assertNodesExist(null, "/branch1", "/branch1/child1"); + assertPropExists(null, "/branch1/child1", "prop1"); + assertPropExists(null, "/branch1/child1", "prop2"); + } + + @Test + public void twoBranchesAddedChildren1() { + addNodes(null, "/trunk", "/trunk/child1"); + assertNodesExist(null, "/trunk", "/trunk/child1"); + + String branchRev1 = mk.branch(null); + String branchRev2 = mk.branch(null); + + branchRev1 = addNodes(branchRev1, "/branch1", "/branch1/child1"); + branchRev2 = addNodes(branchRev2, "/branch2", "/branch2/child2"); + assertNodesExist(branchRev1, "/trunk", "/trunk/child1"); + assertNodesExist(branchRev2, "/trunk", "/trunk/child1"); + assertNodesExist(branchRev1, "/branch1/child1"); + assertNodesNotExist(branchRev1, "/branch2/child2"); + assertNodesExist(branchRev2, "/branch2/child2"); + assertNodesNotExist(branchRev2, "/branch1/child1"); + assertNodesNotExist(null, "/branch1/child1", "/branch2/child2"); + + addNodes(null, "/trunk/child2"); + assertNodesExist(null, "/trunk/child2"); + assertNodesNotExist(branchRev1, "/trunk/child2"); + assertNodesNotExist(branchRev2, "/trunk/child2"); + + mk.merge(branchRev1, ""); + assertNodesExist(null, "/trunk", "/branch1", "/branch1/child1"); + assertNodesNotExist(null, "/branch2", "/branch2/child2"); + + mk.merge(branchRev2, ""); + assertNodesExist(null, "/trunk", "/branch1", "/branch1/child1", "/branch2", "/branch2/child2"); + } + + @Test + public void oneBranchAddedChildrenWithConflict() { + addNodes(null, "/trunk", "/trunk/child1"); + assertNodesExist(null, "/trunk", "/trunk/child1"); + + String branchRev = mk.branch(null); + + branchRev = removeNodes(branchRev, "/trunk/child1"); + assertNodesExist(branchRev, "/trunk"); + assertNodesNotExist(branchRev, "/trunk/child1"); + + addNodes(null, "/trunk/child1/child2"); + assertNodesExist(null, "/trunk", "/trunk/child1", "/trunk/child1/child2"); + + mk.merge(branchRev, ""); + assertNodesExist(null, "/trunk"); + assertNodesNotExist(null, "/trunk/child1", "/trunk/child1/child2"); + } + + @Test + public void oneBranchChangedPropertiesWithConflict() { + addNodes(null, "/trunk"); + setProp(null, "/trunk/prop1", "value1"); + assertPropExists(null, "/trunk", "prop1"); + + String branchRev = mk.branch(null); + + branchRev = setProp(branchRev, "/trunk/prop1", "value1a"); + assertPropValue(branchRev, "/trunk", "prop1", "value1a"); + + setProp(null, "/trunk/prop1", "value1b"); + try { + mk.merge(branchRev, ""); + fail("Expected: Concurrent modification exception"); + } catch (Exception expected){} + } + + @Test + public void addExistingRootInBranch() { + addNodes(null, "/root"); + assertNodesExist(null, "/root"); + + String branchRev = mk.branch(null); + try { + branchRev = addNodes(branchRev, "/root"); + fail("Should not be able to add the same root node twice"); + } catch (Exception expected) {} + } + + @Test + public void addExistingChildInBranch() { + addNodes(null, "/root", "/root/child1"); + assertNodesExist(null, "/root", "/root/child1"); + + String branchRev = mk.branch(null); + branchRev = addNodes(branchRev, "/root/child2"); + assertNodesExist(branchRev, "/root/child1", "/root/child2"); + + try { + branchRev = addNodes(branchRev, "/root/child1"); + fail("Should not be able to add the same root node twice"); + } catch (Exception expected) {} + } + + @Test + public void emptyMergeCausesNoChange() { + String rev1 = mk.commit("", "+\"/child1\":{}", null, ""); + + String branchRev = mk.branch(null); + branchRev = mk.commit("", "+\"/child2\":{}", branchRev, ""); + branchRev = mk.commit("", "-\"/child2\"", branchRev, ""); + + String rev2 = mk.merge(branchRev, ""); + + assertTrue(mk.nodeExists("/child1", null)); + assertFalse(mk.nodeExists("/child2", null)); + assertEquals(rev1, rev2); + } + + @Test + public void trunkMergeNotAllowed() { + String rev = mk.commit("", "+\"/child1\":{}", null, ""); + try { + mk.merge(rev, ""); + fail("Exception expected"); + } catch (Exception expected) {} + } + + private String addNodes(String rev, String...nodes) { + String newRev = rev; + for (String node : nodes) { + newRev = mk.commit("", "+\"" + node + "\":{}", rev, ""); + } + return newRev; + } + + private String removeNodes(String rev, String...nodes) { + String newRev = rev; + for (String node : nodes) { + newRev = mk.commit("", "-\"" + node + "\"", rev, ""); + } + return newRev; + } + + private String setProp(String rev, String prop, Object value) { + value = value == null? null : "\"" + value + "\""; + return mk.commit("", "^\"" + prop + "\" : " + value, rev, ""); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKCommitAddTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKCommitAddTest.java new file mode 100644 index 00000000000..87a0015b41c --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKCommitAddTest.java @@ -0,0 +1,113 @@ +package org.apache.jackrabbit.mongomk.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.apache.jackrabbit.mongomk.BaseMongoMicroKernelTest; +import org.json.simple.JSONObject; +import org.junit.Test; + +/** + * Tests for {@link MongoMicroKernel#commit(String, String, String, String)} + * with emphasis on add node and property operations. + */ +public class MongoMKCommitAddTest extends BaseMongoMicroKernelTest { + + @Test + public void addSingleNode() throws Exception { + mk.commit("/", "+\"a\" : {}", null, null); + + long childCount = mk.getChildNodeCount("/", null); + assertEquals(1, childCount); + + String nodes = mk.getNodes("/", null, -1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyValue(obj, ":childNodeCount", 1L); + } + + @Test + public void addNodeWithChildren() throws Exception { + mk.commit("/", "+\"a\" : { \"b\": {}, \"c\": {}, \"d\" : {} }", null, null); + + assertTrue(mk.nodeExists("/a",null)); + assertTrue(mk.nodeExists("/a/b",null)); + assertTrue(mk.nodeExists("/a/c",null)); + assertTrue(mk.nodeExists("/a/d",null)); + } + + @Test + public void addNodeWithNestedChildren() throws Exception { + mk.commit("/", "+\"a\" : { \"b\": { \"c\" : { \"d\" : {} } } }", null, null); + + assertTrue(mk.nodeExists("/a",null)); + assertTrue(mk.nodeExists("/a/b",null)); + assertTrue(mk.nodeExists("/a/b/c",null)); + assertTrue(mk.nodeExists("/a/b/c/d",null)); + } + + + @Test + public void addIntermediataryNodes() throws Exception { + String rev1 = mk.commit("/", "+\"a\" : { \"b\" : { \"c\": {} }}", null, null); + String rev2 = mk.commit("/", "+\"a/d\" : {} +\"a/b/e\" : {}", null, null); + + assertTrue(mk.nodeExists("/a/b/c", rev1)); + assertFalse(mk.nodeExists("/a/b/e", rev1)); + assertFalse(mk.nodeExists("/a/d", rev1)); + + assertTrue(mk.nodeExists("/a/b/c", rev2)); + assertTrue(mk.nodeExists("/a/b/e", rev2)); + assertTrue(mk.nodeExists("/a/d", rev2)); + } + + @Test + public void addDuplicateNode() throws Exception { + mk.commit("/", "+\"a\" : {}", null, null); + try { + mk.commit("/", "+\"a\" : {}", null, null); + fail("Exception expected"); + } catch (Exception expected) {} + } + + @Test + public void setSingleProperty() throws Exception { + mk.commit("/", "+\"a\" : {} ^\"a/key1\" : \"value1\"", null, null); + + long childCount = mk.getChildNodeCount("/", null); + assertEquals(1, childCount); + + String nodes = mk.getNodes("/", null, 1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyValue(obj, ":childNodeCount", 1L); + assertPropertyValue(obj, "a/key1", "value1"); + } + + @Test + public void setMultipleProperties() throws Exception { + mk.commit("/", "+\"a\" : {} ^\"a/key1\" : \"value1\"", null, null); + mk.commit("/", "^\"a/key2\" : 2", null, null); + mk.commit("/", "^\"a/key3\" : false", null, null); + mk.commit("/", "^\"a/key4\" : 0.25", null, null); + + long childCount = mk.getChildNodeCount("/", null); + assertEquals(1, childCount); + + String nodes = mk.getNodes("/", null, 1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyValue(obj, ":childNodeCount", 1L); + assertPropertyValue(obj, "a/key1", "value1"); + assertPropertyValue(obj, "a/key2", 2L); + assertPropertyValue(obj, "a/key3", false); + assertPropertyValue(obj, "a/key4", 0.25); + } + + @Test + public void setPropertyWithoutAddingNode() throws Exception { + try { + mk.commit("/", "^\"a/key1\" : \"value1\"", null, null); + fail("Exception expected"); + } catch (Exception expected) {} + } +} \ No newline at end of file diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKCommitCopyTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKCommitCopyTest.java new file mode 100644 index 00000000000..46f30f3db47 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKCommitCopyTest.java @@ -0,0 +1,259 @@ +package org.apache.jackrabbit.mongomk.impl; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.apache.jackrabbit.mongomk.BaseMongoMicroKernelTest; +import org.json.simple.JSONObject; +import org.junit.Test; + +/** + * Tests for {@link MongoMicroKernel#commit(String, String, String, String)} + * with emphasis on copy operations. + */ +public class MongoMKCommitCopyTest extends BaseMongoMicroKernelTest { + + @Test + public void copyNode() throws Exception { + mk.commit("/", "+\"a\" : {}", null, null); + assertTrue(mk.nodeExists("/a", null)); + + mk.commit("/", "*\"a\" : \"b\"", null, null); + assertTrue(mk.nodeExists("/a", null)); + assertTrue(mk.nodeExists("/b", null)); + } + + @Test + public void copyNodeWithChild() throws Exception { + mk.commit("/", "+\"a\" : { \"b\" : {} }", null, null); + assertTrue(mk.nodeExists("/a", null)); + assertTrue(mk.nodeExists("/a/b", null)); + + mk.commit("/", "*\"a\" : \"c\"", null, null); + assertTrue(mk.nodeExists("/a", null)); + assertTrue(mk.nodeExists("/a/b", null)); + assertTrue(mk.nodeExists("/c", null)); + assertTrue(mk.nodeExists("/c/b", null)); + } + + @Test + public void copyNodeWithChildren() throws Exception { + mk.commit("/", "+\"a\" : { \"b\" : {}, \"c\" : {}, \"d\" : {}}", null, null); + assertTrue(mk.nodeExists("/a", null)); + assertTrue(mk.nodeExists("/a/b", null)); + assertTrue(mk.nodeExists("/a/c", null)); + assertTrue(mk.nodeExists("/a/d", null)); + + mk.commit("/", "*\"a\" : \"e\"", null, null); + assertTrue(mk.nodeExists("/e", null)); + assertTrue(mk.nodeExists("/e/b", null)); + assertTrue(mk.nodeExists("/e/c", null)); + assertTrue(mk.nodeExists("/e/d", null)); + } + + @Test + public void copyNodeWithNestedChildren() throws Exception { + mk.commit("/", "+\"a\" : { \"b\" : { \"c\" : { \"d\" : {} } } }", null, null); + assertTrue(mk.nodeExists("/a", null)); + assertTrue(mk.nodeExists("/a/b", null)); + assertTrue(mk.nodeExists("/a/b/c", null)); + assertTrue(mk.nodeExists("/a/b/c/d", null)); + + mk.commit("/", "*\"a\" : \"e\"", null, null); + assertTrue(mk.nodeExists("/e", null)); + assertTrue(mk.nodeExists("/e/b", null)); + assertTrue(mk.nodeExists("/e/b/c", null)); + assertTrue(mk.nodeExists("/e/b/c/d", null)); + + mk.commit("/", "*\"e/b\" : \"f\"", null, null); + assertTrue(mk.nodeExists("/f", null)); + assertTrue(mk.nodeExists("/f/c", null)); + assertTrue(mk.nodeExists("/f/c/d", null)); + } + + @Test + public void copyNodeWithProperties() throws Exception { + mk.commit("/", "+\"a\" : { \"key1\" : \"value1\" }", null, null); + assertTrue(mk.nodeExists("/a", null)); + String nodes = mk.getNodes("/", null, 1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyValue(obj, "a/key1", "value1"); + + mk.commit("/", "*\"a\" : \"c\"", null, null); + assertTrue(mk.nodeExists("/c", null)); + nodes = mk.getNodes("/", null, 1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + obj = parseJSONObject(nodes); + assertPropertyValue(obj, "c/key1", "value1"); + } + + @Test + public void copyFromNonExistentNode() throws Exception { + mk.commit("/", "+\"a\" : {}", null, null); + assertTrue(mk.nodeExists("/a", null)); + + try { + mk.commit("/", "*\"b\" : \"c\"", null, null); + fail("Exception expected"); + } catch (Exception expected) {} + } + + @Test + public void copyToAnExistentNode() throws Exception { + mk.commit("/", "+\"a\" : { \"b\" : {} }", null, null); + mk.commit("/", "+\"c\" : {}", null, null); + + try { + mk.commit("/", "*\"c\" : \"a/b\"", null, null); + fail("Exception expected"); + } catch (Exception expected) {} + } + + @Test + public void addNodeAndCopy() { + mk.commit("/", "+\"a\":{}", null, null); + mk.commit("/", "+\"a/b\":{}\n" + + "*\"a/b\":\"c\"", null, null); + + assertTrue(mk.nodeExists("/a/b", null)); + assertTrue(mk.nodeExists("/c", null)); + } + + @Test + public void addNodeWithChildrenAndCopy() { + mk.commit("/", "+\"a\":{}", null, null); + mk.commit("/", "+\"a/b\":{ \"c\" : {}, \"d\" : {} }\n" + + "*\"a/b\":\"e\"", null, null); + + assertTrue(mk.nodeExists("/a/b/c", null)); + assertTrue(mk.nodeExists("/a/b/d", null)); + assertTrue(mk.nodeExists("/e/c", null)); + assertTrue(mk.nodeExists("/e/d", null)); + } + + @Test + public void addNodeWithNestedChildrenAndCopy() { + mk.commit("/", "+\"a\":{ \"b\" : { \"c\" : { } } }", null, null); + mk.commit("/", "+\"a/b/c/d\":{}\n" + + "*\"a\":\"e\"", null, null); + + assertTrue(mk.nodeExists("/a/b/c/d", null)); + assertTrue(mk.nodeExists("/e/b/c/d", null)); + } + + @Test + public void addNodeAndCopyParent() { + mk.commit("/", "+\"a\":{}", null, null); + mk.commit("/", "+\"a/b\":{}\n" + + "*\"a\":\"c\"", null, null); + + assertTrue(mk.nodeExists("/a/b", null)); + assertTrue(mk.nodeExists("/c/b", null)); + } + + @Test + public void removeNodeAndCopy() { + mk.commit("/", "+\"a\":{ \"b\" : {} }", null, null); + + try { + mk.commit("/", "-\"a/b\"\n" + + "*\"a/b\":\"c\"", null, null); + fail("Expected expected"); + } catch (Exception expected) {} + } + + @Test + public void removeNodeWithNestedChildrenAndCopy() { + mk.commit("/", "+\"a\":{ \"b\" : { \"c\" : { \"d\" : {} } } }", null, null); + mk.commit("/", "-\"a/b/c/d\"\n" + + "*\"a\" : \"e\"", null, null); + + assertFalse(mk.nodeExists("/a/b/c/d", null)); + assertTrue(mk.nodeExists("/e/b/c", null)); + assertFalse(mk.nodeExists("/e/b/c/d", null)); + } + + @Test + public void removeNodeAndCopyParent() { + mk.commit("/", "+\"a\":{ \"b\" : {} }", null, null); + mk.commit("/", "-\"a/b\"\n" + + "*\"a\":\"c\"", null, null); + + assertFalse(mk.nodeExists("/a/b", null)); + assertFalse(mk.nodeExists("/c/b", null)); + } + + @Test + public void setPropertyAndCopy() { + mk.commit("/", "+\"a\":{}", null, null); + mk.commit("/", "^\"a/key1\": \"value1\"\n" + + "*\"a\":\"c\"", null, null); + + String nodes = mk.getNodes("/", null, 1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyValue(obj, "a/key1", "value1"); + assertPropertyValue(obj, "c/key1", "value1"); + } + + @Test + public void setNestedPropertyAndCopy() { + mk.commit("/", "+\"a\":{ \"b\" : {} }", null, null); + mk.commit("/", "^\"a/b/key1\": \"value1\"\n" + + "*\"a\":\"c\"", null, null); + + String nodes = mk.getNodes("/", null, 2 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyValue(obj, "a/b/key1", "value1"); + assertPropertyValue(obj, "c/b/key1", "value1"); + } + + @Test + public void modifyParentAddPropertyAndCopy() { + mk.commit("/", "+\"a\":{}", null, null); + mk.commit("/", "+\"b\" : {}\n" + + "^\"a/key1\": \"value1\"\n" + + "*\"a\":\"c\"", null, null); + + String nodes = mk.getNodes("/", null, 1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyValue(obj, "a/key1", "value1"); + assertPropertyValue(obj, "c/key1", "value1"); + } + + @Test + public void removePropertyAndCopy() { + mk.commit("/", "+\"a\":{ \"b\" : { \"key1\" : \"value1\" } }", null, null); + mk.commit("/", "^\"a/b/key1\": null\n" + + "*\"a\":\"c\"", null, null); + + String nodes = mk.getNodes("/", null, 1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyNotExists(obj, "a/b/key1"); + assertPropertyNotExists(obj, "c/b/key1"); + } + + @Test + public void removeNestedPropertyAndCopy() { + mk.commit("/", "+\"a\":{ \"key1\" : \"value1\"}", null, null); + mk.commit("/", "^\"a/key1\" : null\n" + + "*\"a\":\"c\"", null, null); + + String nodes = mk.getNodes("/", null, 1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyNotExists(obj, "a/key1"); + assertPropertyNotExists(obj, "c/key1"); + } + + @Test + public void modifyParentRemovePropertyAndCopy() { + mk.commit("/", "+\"a\":{ \"key1\" : \"value1\"}", null, null); + mk.commit("/", "+\"b\" : {}\n" + + "^\"a/key1\" : null\n" + + "*\"a\":\"c\"", null, null); + + String nodes = mk.getNodes("/", null, 1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyNotExists(obj, "a/key1"); + assertPropertyNotExists(obj, "c/key1"); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKCommitMoveTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKCommitMoveTest.java new file mode 100644 index 00000000000..6b47dcca039 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKCommitMoveTest.java @@ -0,0 +1,307 @@ +package org.apache.jackrabbit.mongomk.impl; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.apache.jackrabbit.mongomk.BaseMongoMicroKernelTest; +import org.json.simple.JSONObject; +import org.junit.Test; + +/** + * Tests for {@link MongoMicroKernel#commit(String, String, String, String)} + * with emphasis on move operations. + */ +public class MongoMKCommitMoveTest extends BaseMongoMicroKernelTest { + + @Test + public void moveNode() throws Exception { + mk.commit("/", "+\"a\" : {}", null, null); + assertTrue(mk.nodeExists("/a", null)); + + mk.commit("/", ">\"a\" : \"b\"", null, null); + assertFalse(mk.nodeExists("/a", null)); + assertTrue(mk.nodeExists("/b", null)); + } + + @Test + public void moveUnderSourcePath() throws Exception { + mk.commit("/", "+\"a\" : { \"b\" : {} }", null, null); + assertTrue(mk.nodeExists("/a", null)); + assertTrue(mk.nodeExists("/a/b", null)); + + try { + mk.commit("/", ">\"b\" : \"a\"", null, null); + fail("Exception expected"); + } catch (Exception expected) {} + } + + @Test + public void moveNodeWithChild() throws Exception { + mk.commit("/", "+\"a\" : { \"b\" : {} }", null, null); + assertTrue(mk.nodeExists("/a", null)); + assertTrue(mk.nodeExists("/a/b", null)); + + mk.commit("/", ">\"a\" : \"c\"", null, null); + assertFalse(mk.nodeExists("/a", null)); + assertFalse(mk.nodeExists("/a/b", null)); + assertTrue(mk.nodeExists("/c", null)); + assertTrue(mk.nodeExists("/c/b", null)); + } + + @Test + public void moveNodeWithChildren() throws Exception { + mk.commit("/", "+\"a\" : { \"b\" : {}, \"c\" : {}, \"d\" : {}}", null, null); + assertTrue(mk.nodeExists("/a", null)); + assertTrue(mk.nodeExists("/a/b", null)); + assertTrue(mk.nodeExists("/a/c", null)); + assertTrue(mk.nodeExists("/a/d", null)); + + mk.commit("/", ">\"a\" : \"e\"", null, null); + assertFalse(mk.nodeExists("/a", null)); + assertFalse(mk.nodeExists("/a/b", null)); + assertFalse(mk.nodeExists("/a/c", null)); + assertFalse(mk.nodeExists("/a/d", null)); + assertTrue(mk.nodeExists("/e", null)); + assertTrue(mk.nodeExists("/e/b", null)); + assertTrue(mk.nodeExists("/e/c", null)); + assertTrue(mk.nodeExists("/e/d", null)); + } + + @Test + public void moveNodeWithNestedChildren() throws Exception { + mk.commit("/", "+\"a\" : { \"b\" : { \"c\" : { \"d\" : {} } } }", null, null); + assertTrue(mk.nodeExists("/a", null)); + assertTrue(mk.nodeExists("/a/b", null)); + assertTrue(mk.nodeExists("/a/b/c", null)); + assertTrue(mk.nodeExists("/a/b/c/d", null)); + + mk.commit("/", ">\"a\" : \"e\"", null, null); + assertFalse(mk.nodeExists("/a", null)); + assertFalse(mk.nodeExists("/a/b", null)); + assertFalse(mk.nodeExists("/a/b/c", null)); + assertFalse(mk.nodeExists("/a/b/c/d", null)); + assertTrue(mk.nodeExists("/e", null)); + assertTrue(mk.nodeExists("/e/b", null)); + assertTrue(mk.nodeExists("/e/b/c", null)); + assertTrue(mk.nodeExists("/e/b/c/d", null)); + + mk.commit("/", ">\"e/b\" : \"f\"", null, null); + assertTrue(mk.nodeExists("/e", null)); + assertFalse(mk.nodeExists("/e/b", null)); + assertFalse(mk.nodeExists("/e/b/c", null)); + assertFalse(mk.nodeExists("/e/b/c/d", null)); + assertTrue(mk.nodeExists("/f", null)); + assertTrue(mk.nodeExists("/f/c", null)); + assertTrue(mk.nodeExists("/f/c/d", null)); + } + + @Test + public void moveNodeWithProperties() throws Exception { + mk.commit("/", "+\"a\" : { \"key1\" : \"value1\" }", null, null); + assertTrue(mk.nodeExists("/a", null)); + String nodes = mk.getNodes("/", null, 1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyValue(obj, "a/key1", "value1"); + + mk.commit("/", ">\"a\" : \"c\"", null, null); + assertFalse(mk.nodeExists("/a", null)); + assertTrue(mk.nodeExists("/c", null)); + nodes = mk.getNodes("/", null, 1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + obj = parseJSONObject(nodes); + assertPropertyValue(obj, "c/key1", "value1"); + } + + @Test + public void moveFromNonExistentNode() throws Exception { + try { + mk.commit("/", ">\"b\" : \"c\"", null, null); + fail("Exception expected"); + } catch (Exception expected) {} + } + + @Test + public void moveToAnExistentNode() throws Exception { + mk.commit("/", "+\"a\" : { \"b\" : {} }", null, null); + mk.commit("/", "+\"c\" : {}", null, null); + + try { + mk.commit("/", ">\"c\" : \"a/b\"", null, null); + fail("Exception expected"); + } catch (Exception expected) {} + } + + @Test + public void addNodeAndMove() { + mk.commit("/", "+\"a\":{}", null, null); + mk.commit("/", "+\"a/b\": {}\n" + + ">\"a/b\":\"c\"", null, null); + + assertFalse(mk.nodeExists("/a/b", null)); + assertTrue(mk.nodeExists("/a", null)); + assertTrue(mk.nodeExists("/c", null)); + } + + @Test + public void addNodeWithChildrenAndMove() { + mk.commit("/", "+\"a\":{}", null, null); + mk.commit("/", "+\"a/b\":{ \"c\" : {}, \"d\" : {} }\n" + + ">\"a/b\":\"e\"", null, null); + + assertTrue(mk.nodeExists("/a", null)); + assertFalse(mk.nodeExists("/a/b", null)); + assertFalse(mk.nodeExists("/a/b/c", null)); + assertFalse(mk.nodeExists("/a/b/d", null)); + + assertTrue(mk.nodeExists("/e", null)); + assertTrue(mk.nodeExists("/e/c", null)); + assertTrue(mk.nodeExists("/e/d", null)); + } + + @Test + public void addNodeWithNestedChildrenAndMove() { + mk.commit("/", "+\"a\":{ \"b\" : { \"c\" : { } } }", null, null); + mk.commit("/", "+\"a/b/c/d\":{}\n" + + ">\"a\":\"e\"", null, null); + + assertFalse(mk.nodeExists("/a/b/c/d", null)); + assertTrue(mk.nodeExists("/e/b/c/d", null)); + } + + @Test + public void addNodeAndMoveParent() { + mk.commit("/", "+\"a\":{}", null, null); + mk.commit("/", "+\"a/b\":{}\n" + + ">\"a\":\"c\"", null, null); + + assertFalse(mk.nodeExists("/a", null)); + assertFalse(mk.nodeExists("/a/b", null)); + assertTrue(mk.nodeExists("/c", null)); + assertTrue(mk.nodeExists("/c/b", null)); + } + + @Test + public void removeNodeAndMove() { + mk.commit("/", "+\"a\":{ \"b\" : {} }", null, null); + + try { + mk.commit("/", "-\"a/b\"\n" + + ">\"a/b\":\"c\"", null, null); + fail("Expected expected"); + } catch (Exception expected) {} + } + + @Test + public void removeNodeWithNestedChildrenAndMove() { + mk.commit("/", "+\"a\":{ \"b\" : { \"c\" : { \"d\" : {} } } }", null, null); + mk.commit("/", "-\"a/b/c/d\"\n" + + ">\"a\" : \"e\"", null, null); + + assertFalse(mk.nodeExists("/a", null)); + assertTrue(mk.nodeExists("/e/b/c", null)); + assertFalse(mk.nodeExists("/e/b/c/d", null)); + } + + @Test + public void removeNodeAndMoveParent() { + mk.commit("/", "+\"a\":{ \"b\" : {} }", null, null); + mk.commit("/", "-\"a/b\"\n" + + ">\"a\":\"c\"", null, null); + + assertFalse(mk.nodeExists("/a/b", null)); + assertTrue(mk.nodeExists("/c", null)); + assertFalse(mk.nodeExists("/c/b", null)); + } + + @Test + public void setPropertyAndMove() { + mk.commit("/", "+\"a\":{}", null, null); + mk.commit("/", "^\"a/key1\": \"value1\"\n" + + ">\"a\":\"c\"", null, null); + + assertFalse(mk.nodeExists("/a", null)); + assertTrue(mk.nodeExists("/c", null)); + + String nodes = mk.getNodes("/", null, 1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyValue(obj, "c/key1", "value1"); + } + + @Test + public void setNestedPropertyAndMove() { + mk.commit("/", "+\"a\":{ \"b\" : {} }", null, null); + mk.commit("/", "^\"a/b/key1\": \"value1\"\n" + + ">\"a\":\"c\"", null, null); + + assertFalse(mk.nodeExists("/a", null)); + assertFalse(mk.nodeExists("/a/b", null)); + assertTrue(mk.nodeExists("/c", null)); + assertTrue(mk.nodeExists("/c/b", null)); + + String nodes = mk.getNodes("/", null, 2 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyValue(obj, "c/b/key1", "value1"); + } + + @Test + public void modifyParentAddPropertyAndMove() { + mk.commit("/", "+\"a\":{}", null, null); + mk.commit("/", "+\"b\" : {}\n" + + "^\"a/key1\": \"value1\"\n" + + ">\"a\":\"c\"", null, null); + + assertFalse(mk.nodeExists("/a", null)); + assertTrue(mk.nodeExists("/b", null)); + assertTrue(mk.nodeExists("/c", null)); + + String nodes = mk.getNodes("/", null, 1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyValue(obj, "c/key1", "value1"); + } + + @Test + public void removePropertyAndMove() { + mk.commit("/", "+\"a\":{ \"b\" : { \"key1\" : \"value1\" } }", null, null); + mk.commit("/", "^\"a/b/key1\": null\n" + + ">\"a\":\"c\"", null, null); + + assertFalse(mk.nodeExists("/a", null)); + assertFalse(mk.nodeExists("/a/b", null)); + assertTrue(mk.nodeExists("/c", null)); + assertTrue(mk.nodeExists("/c/b", null)); + + String nodes = mk.getNodes("/", null, 1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyNotExists(obj, "c/b/key1"); + } + + @Test + public void removeNestedPropertyAndMove() { + mk.commit("/", "+\"a\":{ \"key1\" : \"value1\"}", null, null); + mk.commit("/", "^\"a/key1\" : null\n" + + ">\"a\":\"c\"", null, null); + + assertFalse(mk.nodeExists("/a", null)); + assertTrue(mk.nodeExists("/c", null)); + + String nodes = mk.getNodes("/", null, 1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyNotExists(obj, "c/key1"); + } + + @Test + public void modifyParentRemovePropertyAndMove() { + mk.commit("/", "+\"a\":{ \"key1\" : \"value1\"}", null, null); + mk.commit("/", "+\"b\" : {}\n" + + "^\"a/key1\" : null\n" + + ">\"a\":\"c\"", null, null); + + assertFalse(mk.nodeExists("/a", null)); + assertTrue(mk.nodeExists("/b", null)); + assertTrue(mk.nodeExists("/c", null)); + + String nodes = mk.getNodes("/", null, 1 /*depth*/, 0 /*offset*/, -1 /*maxChildNodes*/, null /*filter*/); + JSONObject obj = parseJSONObject(nodes); + assertPropertyNotExists(obj, "c/key1"); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKCommitRemoveTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKCommitRemoveTest.java new file mode 100644 index 00000000000..8185547f831 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKCommitRemoveTest.java @@ -0,0 +1,34 @@ +package org.apache.jackrabbit.mongomk.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.apache.jackrabbit.mongomk.BaseMongoMicroKernelTest; +import org.junit.Test; + +/** + * Tests for {@link MongoMicroKernel#commit(String, String, String, String)} + * with emphasis on remove node and property operations. + */ +public class MongoMKCommitRemoveTest extends BaseMongoMicroKernelTest { + + @Test + public void removeSingleNode() throws Exception { + mk.commit("/", "+\"a\" : {}", null, null); + + long childCount = mk.getChildNodeCount("/", null); + assertEquals(1, childCount); + + mk.commit("/", "-\"a\"", null, null); + childCount = mk.getChildNodeCount("/", null); + assertEquals(0, childCount); + } + + @Test + public void removeNonExistentNode() throws Exception { + try { + mk.commit("/", "-\"a\"", null, null); + fail("Exception expected"); + } catch (Exception expected) {} + } +} \ No newline at end of file diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKDiffTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKDiffTest.java new file mode 100644 index 00000000000..8b8dc4e0ed7 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKDiffTest.java @@ -0,0 +1,220 @@ +package org.apache.jackrabbit.mongomk.impl; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.apache.jackrabbit.mongomk.BaseMongoMicroKernelTest; +import org.json.simple.JSONObject; +import org.junit.Test; + +/** + * Tests for MicroKernel#diff + */ +public class MongoMKDiffTest extends BaseMongoMicroKernelTest { + + @Test + public void addPathOneLevel() { + String rev0 = mk.getHeadRevision(); + + String rev1 = mk.commit("/", "+\"level1\":{}", null, null); + assertTrue(mk.nodeExists("/level1", null)); + + String reverseDiff = mk.diff(rev1, rev0, null, -1); + assertNotNull(reverseDiff); + assertTrue(reverseDiff.length() > 0); + + mk.commit("", reverseDiff, null, null); + assertFalse(mk.nodeExists("/level1", null)); + } + + @Test + public void addPathTwoLevels() { + String rev0 = mk.getHeadRevision(); + + String rev1 = mk.commit("/", "+\"level1\":{}", null, null); + rev1 = mk.commit("/", "+\"level1/level2\":{}", null, null); + assertTrue(mk.nodeExists("/level1", null)); + assertTrue(mk.nodeExists("/level1/level2", null)); + + String reverseDiff = mk.diff(rev1, rev0, null, -1); + assertNotNull(reverseDiff); + assertTrue(reverseDiff.length() > 0); + + mk.commit("", reverseDiff, null, null); + assertFalse(mk.nodeExists("/level1", null)); + assertFalse(mk.nodeExists("/level1/level2", null)); + } + + @Test + public void addPathTwoSameLevels() { + String rev0 = mk.getHeadRevision(); + + String rev1 = mk.commit("/", "+\"level1a\":{}", null, null); + rev1 = mk.commit("/", "+\"level1b\":{}", null, null); + assertTrue(mk.nodeExists("/level1a", null)); + assertTrue(mk.nodeExists("/level1b", null)); + + String reverseDiff = mk.diff(rev1, rev0, null, -1); + assertNotNull(reverseDiff); + assertTrue(reverseDiff.length() > 0); + + mk.commit("", reverseDiff, null, null); + assertFalse(mk.nodeExists("/level1a", null)); + assertFalse(mk.nodeExists("/level1b", null)); + } + + @Test + public void removePath() { + // Add level1 & level1/level2 + String rev0 = mk.commit("/","+\"level1\":{}" + + "+\"level1/level2\":{}", null, null); + assertTrue(mk.nodeExists("/level1", null)); + assertTrue(mk.nodeExists("/level1/level2", null)); + + // Remove level1/level2 + String rev1 = mk.commit("/", "-\"level1/level2\"", null, null); + assertTrue(mk.nodeExists("/level1", null)); + assertFalse(mk.nodeExists("/level1/level2", null)); + + // Generate reverseDiff from rev1 to rev0 + String reverseDiff = mk.diff(rev1, rev0, null, -1); + assertNotNull(reverseDiff); + assertTrue(reverseDiff.length() > 0); + + // Commit the reverseDiff and check rev0 state is restored + mk.commit("", reverseDiff, null, null); + assertTrue(mk.nodeExists("/level1", null)); + assertTrue(mk.nodeExists("/level1/level2", null)); + + // Remove level1 at rev0 + String rev2 = mk.commit("/", "-\"level1\"", rev0, null); + assertFalse(mk.nodeExists("/level1", null)); + assertFalse(mk.nodeExists("/level1/level2", null)); + + // Generate reverseDiff from rev2 to rev0 + reverseDiff = mk.diff(rev2, rev0, null, -1); + assertNotNull(reverseDiff); + assertTrue(reverseDiff.length() > 0); + + // Commit the reverseDiff and check rev0 state is restored + mk.commit("", reverseDiff, null, null); + assertTrue(mk.nodeExists("/level1", null)); + assertTrue(mk.nodeExists("/level1/level2", null)); + } + + @Test + public void movePath() { + String rev1 = mk.commit("/", "+\"level1\":{}", null, null); + rev1 = mk.commit("/", "+\"level1/level2\":{}", null, null); + assertTrue(mk.nodeExists("/level1", null)); + assertTrue(mk.nodeExists("/level1/level2", null)); + + String rev2 = mk.commit("/", ">\"level1\" : \"level1new\"", null, null); + assertFalse(mk.nodeExists("/level1", null)); + assertTrue(mk.nodeExists("/level1new", null)); + assertTrue(mk.nodeExists("/level1new/level2", null)); + + String reverseDiff = mk.diff(rev2, rev1, null, -1); + assertNotNull(reverseDiff); + assertTrue(reverseDiff.length() > 0); + + mk.commit("", reverseDiff, null, null); + assertTrue(mk.nodeExists("/level1", null)); + assertTrue(mk.nodeExists("/level1/level2", null)); + assertFalse(mk.nodeExists("/level1new", null)); + assertFalse(mk.nodeExists("/level1new/level2", null)); + } + + @Test + public void copyPath() { + String rev1 = mk.commit("/", "+\"level1\":{}", null, null); + rev1 = mk.commit("/", "+\"level1/level2\":{}", null, null); + assertTrue(mk.nodeExists("/level1", null)); + assertTrue(mk.nodeExists("/level1/level2", null)); + + String rev2 = mk.commit("/", "*\"level1\" : \"level1new\"", null, null); + assertTrue(mk.nodeExists("/level1", null)); + assertTrue(mk.nodeExists("/level1new", null)); + assertTrue(mk.nodeExists("/level1new/level2", null)); + + String reverseDiff = mk.diff(rev2, rev1, null, -1); + assertNotNull(reverseDiff); + assertTrue(reverseDiff.length() > 0); + + mk.commit("", reverseDiff, null, null); + assertTrue(mk.nodeExists("/level1", null)); + assertTrue(mk.nodeExists("/level1/level2", null)); + assertFalse(mk.nodeExists("/level1new", null)); + assertFalse(mk.nodeExists("/level1new/level2", null)); + } + + @Test + public void setProperty() { + String rev0 = mk.commit("/", "+\"level1\":{}", null, null); + assertTrue(mk.nodeExists("/level1", null)); + + // Add property. + String rev1 = mk.commit("/", "^\"level1/prop1\": \"value1\"", null, null); + JSONObject obj = parseJSONObject(mk.getNodes("/level1", null, 1, 0, -1, null)); + assertPropertyExists(obj, "prop1"); + + // Generate reverseDiff from rev1 to rev0 + String reverseDiff = mk.diff(rev1, rev0, null, -1); + assertNotNull(reverseDiff); + assertTrue(reverseDiff.length() > 0); + + // Commit the reverseDiff and check property is gone. + mk.commit("", reverseDiff, null, null); + assertTrue(mk.nodeExists("/level1", null)); + obj = parseJSONObject(mk.getNodes("/level1", null, 1, 0, -1, null)); + assertPropertyNotExists(obj, "prop1"); + } + + @Test + public void removeProperty() { + String rev0 = mk.commit("/", "+\"level1\":{ \"prop1\" : \"value\"}", null, null); + assertTrue(mk.nodeExists("/level1", null)); + JSONObject obj = parseJSONObject(mk.getNodes("/level1", null, 1, 0, -1, null)); + assertPropertyExists(obj, "prop1"); + + // Remove property + String rev1 = mk.commit("/", "^\"level1/prop1\" : null", null, null); + assertTrue(mk.nodeExists("/level1", null)); + obj = parseJSONObject(mk.getNodes("/level1", null, 1, 0, -1, null)); + assertPropertyNotExists(obj, "prop1"); + + // Generate reverseDiff from rev1 to rev0 + String reverseDiff = mk.diff(rev1, rev0, null, -1); + assertNotNull(reverseDiff); + assertTrue(reverseDiff.length() > 0); + + // Commit the reverseDiff and check property is added back. + mk.commit("", reverseDiff, null, null); + obj = parseJSONObject(mk.getNodes("/level1", null, 1, 0, -1, null)); + assertPropertyExists(obj, "prop1"); + } + + @Test + public void changeProperty() { + String rev0 = mk.commit("/", "+\"level1\":{ \"prop1\" : \"value1\"}", null, null); + assertTrue(mk.nodeExists("/level1", null)); + JSONObject obj = parseJSONObject(mk.getNodes("/level1", null, 1, 0, -1, null)); + assertPropertyValue(obj, "prop1", "value1"); + + // Change property + String rev1 = mk.commit("/", "^\"level1/prop1\" : \"value2\"", null, null); + obj = parseJSONObject(mk.getNodes("/level1", null, 1, 0, -1, null)); + assertPropertyValue(obj, "prop1", "value2"); + + // Generate reverseDiff from rev1 to rev0 + String reverseDiff = mk.diff(rev1, rev0, null, -1); + assertNotNull(reverseDiff); + assertTrue(reverseDiff.length() > 0); + + // Commit the reverseDiff and check property is set back. + mk.commit("", reverseDiff, null, null); + obj = parseJSONObject(mk.getNodes("/level1", null, 1, 0, -1, null)); + assertPropertyValue(obj, "prop1", "value1"); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKGetChildCountTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKGetChildCountTest.java new file mode 100644 index 00000000000..d40504196b9 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKGetChildCountTest.java @@ -0,0 +1,77 @@ +package org.apache.jackrabbit.mongomk.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.apache.jackrabbit.mongomk.BaseMongoMicroKernelTest; +import org.junit.Test; + +/** + * Tests for {@link MongoMicroKernel#getChildNodeCount(String, String)} + */ +public class MongoMKGetChildCountTest extends BaseMongoMicroKernelTest { + + @Test + public void noChild() throws Exception { + long childCount = mk.getChildNodeCount("/", null); + assertEquals(0, childCount); + } + + @Test + public void singleChild() throws Exception { + mk.commit("/", "+\"a\" : {}", null, null); + + long childCount = mk.getChildNodeCount("/", null); + assertEquals(1, childCount); + } + + @Test + public void multipleChilden() throws Exception { + mk.commit("/", "+\"a\" : { \"b\": {}, \"c\": {}, \"d\" : {} }", null, null); + + long childCount = mk.getChildNodeCount("/", null); + assertEquals(1, childCount); + + childCount = mk.getChildNodeCount("/a", null); + assertEquals(3, childCount); + + childCount = mk.getChildNodeCount("/a/b", null); + assertEquals(0, childCount); + + childCount = mk.getChildNodeCount("/a/c", null); + assertEquals(0, childCount); + + childCount = mk.getChildNodeCount("/a/d", null); + assertEquals(0, childCount); + } + + @Test + public void multipleNestedChildren() throws Exception { + mk.commit("/", "+\"a\" : { \"b\": { \"c\" : { \"d\" : {} } } }", null, null); + + long childCount = mk.getChildNodeCount("/", null); + assertEquals(1, childCount); + + childCount = mk.getChildNodeCount("/a", null); + assertEquals(1, childCount); + + childCount = mk.getChildNodeCount("/a/b", null); + assertEquals(1, childCount); + + childCount = mk.getChildNodeCount("/a/b/c", null); + assertEquals(1, childCount); + + childCount = mk.getChildNodeCount("/a/b/c/d", null); + assertEquals(0, childCount); + } + + @Test + public void nonExistingPath() throws Exception { + mk.commit("/", "+\"a\" : {}", null, null); + + try { + mk.getChildNodeCount("/nonexisting", null); + fail("Expected: non-existing path exception"); + } catch (Exception expected){} + } +} \ No newline at end of file diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKGetHeadRevisionTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKGetHeadRevisionTest.java new file mode 100644 index 00000000000..07ed3bb56a1 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKGetHeadRevisionTest.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.mongomk.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import org.apache.jackrabbit.mongomk.BaseMongoMicroKernelTest; +import org.junit.Test; + +/** + * Tests for {@code MongoMicroKernel#getHeadRevision()}. + */ +public class MongoMKGetHeadRevisionTest extends BaseMongoMicroKernelTest { + + @Test + public void simple() throws Exception { + SimpleNodeScenario scenario = new SimpleNodeScenario(mk); + String rev1 = scenario.create(); + + String rev2 = mk.getHeadRevision(); + assertEquals(rev1, rev2); + + String rev3 = scenario.delete_A(); + assertFalse(rev3 == rev2); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKGetJournalTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKGetJournalTest.java new file mode 100644 index 00000000000..8f47a3e27a3 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKGetJournalTest.java @@ -0,0 +1,41 @@ +package org.apache.jackrabbit.mongomk.impl; + +import static org.junit.Assert.assertEquals; + +import org.apache.jackrabbit.mongomk.BaseMongoMicroKernelTest; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.junit.Test; + +/** + * Tests for {@code MongoMicroKernel#getJournal(String, String, String)} + */ +public class MongoMKGetJournalTest extends BaseMongoMicroKernelTest { + + @Test + public void simple() throws Exception { + String fromDiff = "+\"a\" : {}"; + String fromMsg = "Add /a"; + String fromRev = mk.commit("/", fromDiff, null, fromMsg); + + String toDiff = "+\"b\" : {}"; + String toMsg = "Add /b"; + String toRev = mk.commit("/", toDiff, null, toMsg); + + JSONArray array = parseJSONArray(mk.getJournal(fromRev, toRev, "/")); + assertEquals(2, array.size()); + + JSONObject rev = getObjectArrayEntry(array, 0); + assertPropertyExists(rev, "id", String.class); + assertPropertyExists(rev, "ts", Long.class); + assertPropertyValue(rev, "msg", fromMsg); + assertPropertyValue(rev, "changes", fromDiff); + + rev = getObjectArrayEntry(array, 1); + assertPropertyExists(rev, "id", String.class); + assertPropertyExists(rev, "ts", Long.class); + assertPropertyValue(rev, "msg", toMsg); + assertPropertyValue(rev, "changes", toDiff); + } + +} \ No newline at end of file diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKGetLengthTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKGetLengthTest.java new file mode 100644 index 00000000000..3c22f721c98 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKGetLengthTest.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; + +import org.apache.jackrabbit.mongomk.BaseMongoMicroKernelTest; +import org.junit.Test; + +/** + * Tests for {@code MongoMicroKernel#getLength(String)} + */ +public class MongoMKGetLengthTest extends BaseMongoMicroKernelTest { + + @Test + public void getLength() throws Exception { + int blobLength = 100; + String blobId = createAndWriteBlob(blobLength); + + long length = mk.getLength(blobId); + assertEquals(blobLength, length); + } + + @Test + public void testNonExistentBlobLength() throws Exception { + try { + mk.getLength("nonExistentBlob"); + fail("Exception expected"); + } catch (Exception expected) { + } + } + + private String createAndWriteBlob(int blobLength) { + byte[] blob = createBlob(blobLength); + return writeBlob(blob); + } + + private byte[] createBlob(int blobLength) { + byte[] blob = new byte[blobLength]; + for (int i = 0; i < blob.length; i++) { + blob[i] = (byte)i; + } + return blob; + } + + private String writeBlob(byte[] blob) { + return mk.write(new ByteArrayInputStream(blob)); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKGetNodesTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKGetNodesTest.java new file mode 100644 index 00000000000..0f9fe795d9f --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKGetNodesTest.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl; + +import static org.junit.Assert.fail; + +import org.apache.jackrabbit.mongomk.BaseMongoMicroKernelTest; +import org.json.simple.JSONObject; +import org.junit.Test; + +/** + * Tests for {@code MongoMicroKernel#getHeadRevision()}. + */ +public class MongoMKGetNodesTest extends BaseMongoMicroKernelTest { + + @Test + public void invalidRevision() throws Exception { + try { + mk.getNodes("/", "invalid", 1, 0, -1, null); + fail("Exception expected"); + } catch (Exception expected) {} + } + + @Test + public void afterDelete() throws Exception { + SimpleNodeScenario scenario = new SimpleNodeScenario(mk); + scenario.create(); + + JSONObject root = parseJSONObject(mk.getNodes("/", null, 1, 0, -1, null)); + assertPropertyValue(root, ":childNodeCount", 1L); + + JSONObject a = resolveObjectValue(root, "a"); + assertPropertyValue(a, ":childNodeCount", 2L); + + scenario.delete_A(); + root = parseJSONObject(mk.getNodes("/", null, 1, 0, -1, null)); + assertPropertyValue(root, ":childNodeCount", 0L); + } + + @Test + public void depthNegative() throws Exception { + SimpleNodeScenario scenario = new SimpleNodeScenario(mk); + scenario.create(); + + JSONObject root = parseJSONObject(mk.getNodes("/", null, -1, 0, -1, null)); + assertPropertyValue(root, ":childNodeCount", 1L); + } + + @Test + public void depthZero() throws Exception { + SimpleNodeScenario scenario = new SimpleNodeScenario(mk); + scenario.create(); + + JSONObject root = parseJSONObject(mk.getNodes("/", null, 0, 0, -1, null)); + assertPropertyValue(root, ":childNodeCount", 1L); + + JSONObject a = resolveObjectValue(root, "a"); + assertPropertyNotExists(a, "int"); + } + + @Test + public void depthOne() throws Exception { + SimpleNodeScenario scenario = new SimpleNodeScenario(mk); + scenario.create(); + + JSONObject root = parseJSONObject(mk.getNodes("/", null, 1, 0, -1, null)); + assertPropertyValue(root, ":childNodeCount", 1L); + + JSONObject a = resolveObjectValue(root, "a"); + assertPropertyValue(a, ":childNodeCount", 2L); + assertPropertyValue(a, "int", 1L); + + JSONObject b = resolveObjectValue(a, "b"); + assertPropertyNotExists(b, "string"); + + JSONObject c = resolveObjectValue(a, "c"); + assertPropertyNotExists(c, "bool"); + } + + @Test + public void depthLimitless() throws Exception { + SimpleNodeScenario scenario = new SimpleNodeScenario(mk); + scenario.create(); + + //JSONObject root = parseJSONObject(mk.getNodes("/", null, 3450, 0, -1, null)); + JSONObject root = parseJSONObject(mk.getNodes("/", null, Integer.MAX_VALUE, 0, -1, null)); + assertPropertyValue(root, ":childNodeCount", 1L); + + JSONObject a = resolveObjectValue(root, "a"); + assertPropertyValue(a, ":childNodeCount", 2L); + assertPropertyValue(a, "int", 1L); + + JSONObject b = resolveObjectValue(a, "b"); + assertPropertyValue(b, "string", "foo"); + + JSONObject c = resolveObjectValue(a, "c"); + assertPropertyValue(c, "bool", true); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKGetRevisionHistoryTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKGetRevisionHistoryTest.java new file mode 100644 index 00000000000..cee6f7d2e11 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKGetRevisionHistoryTest.java @@ -0,0 +1,87 @@ +package org.apache.jackrabbit.mongomk.impl; + +import static org.junit.Assert.assertEquals; + +import org.apache.jackrabbit.mongomk.BaseMongoMicroKernelTest; +import org.json.simple.JSONArray; +import org.junit.Test; + +/** + * Tests for {@link MongoMicroKernel#getRevisionHistory(long, int, String)} + */ +public class MongoMKGetRevisionHistoryTest extends BaseMongoMicroKernelTest { + + @Test + public void maxEntriesZero() throws Exception { + mk.commit("/", "+\"a\" : {}", null, null); + JSONArray array = parseJSONArray(mk.getRevisionHistory(0, 0, "/")); + assertEquals(0, array.size()); + } + + @Test + public void maxEntriesLimitless() throws Exception { + int count = 10; + for (int i = 0; i < count; i++) { + mk.commit("/", "+\"a" + i + "\" : {}", null, null); + } + JSONArray array = parseJSONArray(mk.getRevisionHistory(0, -1, "/")); + assertEquals(count + 1, array.size()); + } + + @Test + public void maxEntriesLimited() throws Exception { + int count = 10; + int limit = 4; + + for (int i = 0; i < count; i++) { + mk.commit("/", "+\"a" + i + "\" : {}", null, null); + } + JSONArray array = parseJSONArray(mk.getRevisionHistory(0, limit, "/")); + assertEquals(limit, array.size()); + } + + @Test + public void path() throws Exception { + int count1 = 5; + for (int i = 0; i < count1; i++) { + mk.commit("/", "+\"a" + i + "\" : {}", null, null); + } + JSONArray array = parseJSONArray(mk.getRevisionHistory(0, -1, "/")); + assertEquals(count1 + 1, array.size()); + + + int count2 = 5; + for (int i = 0; i < count2; i++) { + mk.commit("/a1", "+\"b" + i + "\" : {}", null, null); + } + array = parseJSONArray(mk.getRevisionHistory(0, -1, "/")); + assertEquals(count1 + 1 + count2, array.size()); + + array = parseJSONArray(mk.getRevisionHistory(0, -1, "/a1")); + assertEquals(count2 + 1, array.size()); + } + + @Test + public void since() throws Exception { + Thread.sleep(100); // To make sure there's a little delay since the initial commit. + long since1 = System.currentTimeMillis(); + int count1 = 6; + for (int i = 0; i < count1; i++) { + mk.commit("/", "+\"a" + i + "\" : {}", null, null); + } + JSONArray array = parseJSONArray(mk.getRevisionHistory(since1, -1, "/")); + assertEquals(count1, array.size()); + + + long since2 = System.currentTimeMillis(); + int count2 = 4; + for (int i = 0; i < count2; i++) { + mk.commit("/", "+\"b" + i + "\" : {}", null, null); + } + array = parseJSONArray(mk.getRevisionHistory(since2, -1, "/")); + assertEquals(count2, array.size()); + + array = parseJSONArray(mk.getRevisionHistory(since1, -1, "/")); + assertEquals(count1 + count2, array.size()); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKLimitsTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKLimitsTest.java new file mode 100644 index 00000000000..e899a039f24 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKLimitsTest.java @@ -0,0 +1,68 @@ +package org.apache.jackrabbit.mongomk.impl; + +import java.util.Arrays; + +import org.apache.jackrabbit.mongomk.BaseMongoMicroKernelTest; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.junit.Ignore; +import org.junit.Test; + +/** + * FIXME - Look into these tests and see if we want to fix them somehow. + * + * Tests for MongoMicroKernel limits. + */ +public class MongoMKLimitsTest extends BaseMongoMicroKernelTest { + + /** + * This test currently fails due to 1000 char limit in property sizes in + * MongoDB which affects path property. It also slows down as the test + * progresses. + */ + @Test + @Ignore + public void pathLimit() throws Exception { + String path = "/"; + String baseNodeName = "testingtestingtesting"; + int numberOfCommits = 100; + String jsonDiff; + String message; + + for (int i = 0; i < numberOfCommits; i++) { + jsonDiff = "+\"" + baseNodeName + i + "\" : {}"; + message = "Add node n" + i; + mk.commit(path, jsonDiff, null, message); + if (!PathUtils.denotesRoot(path)) { + path += "/"; + } + path += baseNodeName + i; + } + } + + /** + * This currently fails due to 16MB DBObject size limitation from Mongo + * database. + */ + @Test + @Ignore + public void overMaxBSONLimit() throws Exception { + String path = "/"; + String baseNodeName = "N"; + StringBuilder jsonDiff = new StringBuilder(); + String message; + // create a 1 MB property + char[] chars = new char[1024 * 1024]; + + Arrays.fill(chars, '0'); + String content = new String(chars); + // create 16+ MB diff + for (int i = 0; i < 16; i++) { + jsonDiff.append("+\"" + baseNodeName + i + "\" : {\"key\":\"" + + content + "\"}\n"); + } + String diff = jsonDiff.toString(); + message = "Commit diff size " + diff.getBytes().length; + System.out.println(message); + mk.commit(path, diff, null, message); + } +} diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKNodeExistsTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKNodeExistsTest.java new file mode 100644 index 00000000000..e63798af685 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKNodeExistsTest.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.apache.jackrabbit.mongomk.BaseMongoMicroKernelTest; +import org.junit.Test; + +/** + * Tests for {@code MongoMicroKernel#nodeExists(String, String)} + */ +public class MongoMKNodeExistsTest extends BaseMongoMicroKernelTest { + + @Test + public void simple() throws Exception { + SimpleNodeScenario scenario = new SimpleNodeScenario(mk); + String revisionId = scenario.create(); + + boolean exists = mk.nodeExists("/a", revisionId); + assertTrue(exists); + + exists = mk.nodeExists("/a/b", revisionId); + assertTrue(exists); + + revisionId = scenario.delete_A(); + + exists = mk.nodeExists("/a", revisionId); + assertFalse(exists); + + exists = mk.nodeExists("/a/b", revisionId); + assertFalse(exists); + } + + @Test + public void withoutRevisionId() throws Exception { + SimpleNodeScenario scenario = new SimpleNodeScenario(mk); + scenario.create(); + + boolean exists = mk.nodeExists("/a", null); + assertTrue(exists); + + scenario.delete_A(); + + exists = mk.nodeExists("/a", null); + assertFalse(exists); + } + + @Test + public void withInvalidRevisionId() throws Exception { + SimpleNodeScenario scenario = new SimpleNodeScenario(mk); + scenario.create(); + + try { + mk.nodeExists("/a", "123456789"); + fail("Expected: Invalid revision id exception"); + } catch (Exception expected) { + } + } + + @Test + public void parentDelete() throws Exception { + SimpleNodeScenario scenario = new SimpleNodeScenario(mk); + scenario.create(); + + boolean exists = mk.nodeExists("/a/b", null); + assertTrue(exists); + + scenario.delete_A(); + exists = mk.nodeExists("/a/b", null); + assertFalse(exists); + } + + @Test + public void grandParentDelete() throws Exception { + mk.commit("/", "+\"a\" : { \"b\" : { \"c\" : { \"d\" : {} } } }", null, + "Add /a/b/c/d"); + + mk.commit("/a", "-\"b\"", null, "Remove /b"); + + boolean exists = mk.nodeExists("/a/b/c/d", null); + assertFalse(exists); + } + + @Test + public void existsInHeadRevision() throws Exception { + mk.commit("/", "+\"a\" : {}", null, "Add /a"); + mk.commit("/a", "+\"b\" : {}", null, "Add /a/b"); + + boolean exists = mk.nodeExists("/a", null); + assertTrue("The node a is not found in the head revision!", exists); + } + + @Test + public void existsInOldRevNotInNewRev() throws Exception { + SimpleNodeScenario scenario = new SimpleNodeScenario(mk); + String rev1 = scenario.create(); + String rev2 = scenario.delete_A(); + + boolean exists = mk.nodeExists("/a", rev1); + assertTrue(exists); + + exists = mk.nodeExists("/a", rev2); + assertFalse(exists); + } + + @Test + public void siblingDelete() throws Exception { + SimpleNodeScenario scenario = new SimpleNodeScenario(mk); + scenario.create(); + + scenario.delete_B(); + boolean exists = mk.nodeExists("/a/b", null); + assertFalse(exists); + + exists = mk.nodeExists("/a/c", null); + assertTrue(exists); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKReadTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKReadTest.java new file mode 100644 index 00000000000..ed0ec42a144 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKReadTest.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl; + +import java.io.ByteArrayInputStream; +import java.util.Arrays; + +import junit.framework.Assert; + +import org.apache.jackrabbit.mongomk.BaseMongoMicroKernelTest; +import org.junit.Test; + +/** + * Tests for {@code MongoMicroKernel#read(String, long, byte[], int, int)} + */ +public class MongoMKReadTest extends BaseMongoMicroKernelTest { + + private byte[] blob; + private String blobId; + + @Override + public void setUp() throws Exception { + super.setUp(); + + blob = new byte[100]; + for (int i = 0; i < blob.length; i++) { + blob[i] = (byte) i; + } + blobId = mk.write(new ByteArrayInputStream(blob)); + } + + @Test + public void complete() throws Exception { + byte[] buffer = new byte[blob.length]; + int totalBytes = mk.read(blobId, 0, buffer, 0, blob.length); + + Assert.assertEquals(blob.length, totalBytes); + Assert.assertTrue(Arrays.equals(blob, buffer)); + } + + @Test + public void rangeEndFromEnd() throws Exception { + byte[] buffer = new byte[blob.length / 2]; + int totalBytes = mk.read(blobId, (blob.length / 2) - 1, buffer, 0, blob.length / 2); + + Assert.assertEquals(blob.length / 2, totalBytes); + for (int i = 0; i < buffer.length; i++) { + Assert.assertEquals(blob[((blob.length / 2) - 1) + i], buffer[i]); + } + } + + @Test + public void rangeFromStart() throws Exception { + byte[] buffer = new byte[blob.length / 2]; + int totalBytes = mk.read(blobId, 0, buffer, 0, blob.length / 2); + + Assert.assertEquals(blob.length / 2, totalBytes); + for (int i = 0; i < buffer.length; i++) { + Assert.assertEquals(blob[i], buffer[i]); + } + } +} \ No newline at end of file diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKWaitForCommitTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKWaitForCommitTest.java new file mode 100644 index 00000000000..00a8e77a3f2 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKWaitForCommitTest.java @@ -0,0 +1,118 @@ +package org.apache.jackrabbit.mongomk.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.blobs.BlobStore; +import org.apache.jackrabbit.mongomk.BaseMongoMicroKernelTest; +import org.apache.jackrabbit.mongomk.api.NodeStore; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests for {@code MongoMicroKernel#waitForCommit(String, long)} + */ +public class MongoMKWaitForCommitTest extends BaseMongoMicroKernelTest { + + private MicroKernel mk2; + + @Before + public void setUp() throws Exception { + super.setUp(); + NodeStore nodeStore = new NodeStoreMongo(mongoConnection); + BlobStore blobStore = new BlobStoreMongo(mongoConnection); + mk2 = new MongoMicroKernel(nodeStore, blobStore); + } + + @Test + public void timeoutNonPositiveNoCommit() throws Exception { + String headRev = mk.commit("/", "+\"a\" : {}", null, null); + long before = System.currentTimeMillis(); + String rev = mk2.waitForCommit(null, -1); + long after = System.currentTimeMillis(); + assertEquals(headRev, rev); + assertTrue(after - before < 100); // Basically no wait. + } + + @Test + public void timeoutNoCommit() throws Exception { + int timeout = 500; + String headRev = mk.commit("/", "+\"a\" : {}", null, null); + long before = System.currentTimeMillis(); + String rev = mk2.waitForCommit(headRev, timeout); + long after = System.currentTimeMillis(); + assertEquals(headRev, rev); + assertTrue(after - before >= timeout); + } + + @Test + public void timeoutWithCommit1() throws Exception { + String headRev = mk.commit("/", "+\"a\" : {}", null, null); + ScheduledFuture future = scheduleCommit(1000, null); + int timeout = 500; + long before = System.currentTimeMillis(); + String rev = mk2.waitForCommit(headRev, timeout); + long after = System.currentTimeMillis(); + headRev = future.get(); + assertFalse(headRev.equals(rev)); + assertTrue(after - before >= timeout); + } + + @Test + public void timeoutWithCommit2() throws Exception { + String headRev = mk.commit("/", "+\"a\" : {}", null, null); + ScheduledFuture future = scheduleCommit(500, null); + int timeout = 2000; + long before = System.currentTimeMillis(); + String rev = mk2.waitForCommit(headRev, timeout); + long after = System.currentTimeMillis(); + headRev = future.get(); + assertTrue(headRev.equals(rev)); + assertTrue(after - before < timeout); + } + + @Test + public void branchIgnored() throws Exception { + String headRev = mk.commit("/", "+\"a\" : {}", null, null); + String branchRev = mk.branch(headRev); + ScheduledFuture future = scheduleCommit(500, branchRev); + int timeout = 2000; + long before = System.currentTimeMillis(); + String rev = mk2.waitForCommit(headRev, timeout); + long after = System.currentTimeMillis(); + headRev = future.get(); + assertFalse(headRev.equals(rev)); + assertTrue(after - before >= timeout); + } + + @Test + public void nullOldHeadRevisionId() throws Exception { + String headRev = mk.commit("/", "+\"a\" : {}", null, null); + long before = System.currentTimeMillis(); + String rev = mk2.waitForCommit(null, 500); + long after = System.currentTimeMillis(); + assertEquals(headRev, rev); + assertEquals(headRev, rev); + assertTrue(after - before < 10); // Basically no wait. + } + + private ScheduledFuture scheduleCommit(long delay, final String revisionId) { + ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); + ScheduledFuture future = executorService.schedule(new Callable(){ + @Override + public String call() throws Exception { + return mk.commit("/", "+\"b\" : {}", revisionId, null); + } + }, delay, TimeUnit.MILLISECONDS); + executorService.shutdown(); + return future; + } +} \ No newline at end of file diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKWriteTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKWriteTest.java new file mode 100644 index 00000000000..2321ca19ece --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/MongoMKWriteTest.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.mongomk.impl; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.util.Arrays; + +import org.apache.jackrabbit.mongomk.BaseMongoMicroKernelTest; +import org.junit.Test; + +/** + * Tests for {@code MongoMicroKernel#write(java.io.InputStream)} + */ +public class MongoMKWriteTest extends BaseMongoMicroKernelTest { + + @Test + public void complete() throws Exception { + int blobLength = 100; + byte[] blob = createBlob(blobLength); + + String blobId = mk.write(new ByteArrayInputStream(blob)); + assertNotNull(blobId); + + byte[] readBlob = new byte[blobLength]; + mk.read(blobId, 0, readBlob, 0, readBlob.length); + assertTrue(Arrays.equals(blob, readBlob)); + } + + private byte[] createBlob(int blobLength) { + byte[] blob = new byte[blobLength]; + for (int i = 0; i < blob.length; i++) { + blob[i] = (byte)i; + } + return blob; + } +} \ No newline at end of file diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/NodeAssert.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/NodeAssert.java new file mode 100644 index 00000000000..8aa156ca3fb --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/NodeAssert.java @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; + +import junit.framework.Assert; + +import org.apache.jackrabbit.mongomk.api.model.Node; +import org.apache.jackrabbit.oak.commons.PathUtils; + +public class NodeAssert { + + public static void assertDeepEquals(Node expected, Node actual) { + assertEquals(expected, actual); + + int expectedCount = expected.getChildNodeCount(); + int actualCount = actual.getChildNodeCount(); + Assert.assertEquals(expectedCount, actualCount); + + for (Iterator it = expected.getChildNodeEntries(0, -1); it.hasNext(); ) { + Node expectedChild = it.next(); + String expectedChildName = PathUtils.getName(expectedChild.getPath()); + boolean valid = false; + for (Iterator it2 = actual.getChildNodeEntries(0, -1); it2.hasNext(); ) { + Node actualChild = it2.next(); + String actualChildName = PathUtils.getName(actualChild.getPath()); + if (expectedChildName.equals(actualChildName)) { + assertDeepEquals(expectedChild, actualChild); + valid = true; + break; + } + } + + Assert.assertTrue(valid); + } + } + + public static void assertEquals(Iterator expecteds, Collection actuals) { + for (Iterator iter1 = expecteds; iter1.hasNext();) { + Node expected = iter1.next(); + boolean valid = false; + for (Iterator iter2 = actuals.iterator(); iter2.hasNext();) { + Node actual = iter2.next(); + if (expected.getPath().equals(actual.getPath())) { + assertEquals(expected, actual); + valid = true; + break; + } + } + Assert.assertTrue(valid); + } + } + + public static void assertEquals(Collection expecteds, Collection actuals) { + Assert.assertEquals(expecteds.size(), actuals.size()); + + for (Node expected : expecteds) { + boolean valid = false; + for (Node actual : actuals) { + if (expected.getPath().equals(actual.getPath())) { + assertEquals(expected, actual); + valid = true; + + break; + } + } + + Assert.assertTrue(valid); + } + } + + public static void assertEquals(Node expected, Node actual) { + Assert.assertEquals(expected.getPath(), actual.getPath()); + + Long expectedRevisionId = expected.getRevisionId(); + Long actualRevisionId = actual.getRevisionId(); + + if (expectedRevisionId == null) { + Assert.assertNull(actualRevisionId); + } + if (actualRevisionId == null) { + Assert.assertNull(expectedRevisionId); + } + + if ((actualRevisionId != null) && (expectedRevisionId != null)) { + Assert.assertEquals(expectedRevisionId, actualRevisionId); + } + + Map expectedProperties = expected.getProperties(); + Map actualProperties = actual.getProperties(); + + if (expectedProperties == null) { + Assert.assertNull(actualProperties); + } + + if (actualProperties == null) { + Assert.assertNull(expectedProperties); + } + + if ((actualProperties != null) && (expectedProperties != null)) { + Assert.assertEquals(expectedProperties, actualProperties); + } + } +} diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/SimpleNodeScenario.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/SimpleNodeScenario.java new file mode 100644 index 00000000000..38cf2274498 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/SimpleNodeScenario.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl; + +import org.apache.jackrabbit.mk.api.MicroKernel; + +public class SimpleNodeScenario { + + private final MicroKernel mk; + + public SimpleNodeScenario(MicroKernel mk) { + this.mk = mk; + } + + public String create() throws Exception { + return mk.commit("/", + "+\"a\" : { \"int\" : 1 , \"b\" : { \"string\" : \"foo\" } , \"c\" : { \"bool\" : true } }", + null, + "Simple node scenario with nodes /, /a, /a/b, /a/c"); + } + + public String addChildrenToA(int count) throws Exception { + String revisionId = null; + for (int i = 1; i <= count; i++) { + revisionId = mk.commit("/a", "+\"child" + i + "\" : {}", null, "Add child" + i); + } + return revisionId; + } + + public String delete_A() throws Exception { + return mk.commit("/", "-\"a\"", null, "Commit with deleted /a"); + } + + public String delete_B() throws Exception { + return mk.commit("/a", "-\"b\"", null, "Commit with deleted /a/b"); + } + + public String update_A_and_add_D_and_E() throws Exception { + StringBuilder diff = new StringBuilder(); + diff.append("+\"a/d\" : {}"); + diff.append("+\"a/b/e\" : {}"); + diff.append("^\"a/double\" : 0.123"); + diff.append("^\"a/d/int\" : 2"); + diff.append("^\"a/b/e/array\" : [ 123, null, 123.456, \"for:bar\", true ]"); + return mk.commit("/", diff.toString(), null, + "Commit with updated /a and added /a/d and /a/b/e"); + } +} diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/builder/CommitBuilderTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/builder/CommitBuilderTest.java new file mode 100644 index 00000000000..46e3527a6b9 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/builder/CommitBuilderTest.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl.builder; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.util.List; + +import junit.framework.Assert; + +import org.apache.jackrabbit.mongomk.api.instruction.Instruction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.AddNodeInstruction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.CopyNodeInstruction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.MoveNodeInstruction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.RemoveNodeInstruction; +import org.apache.jackrabbit.mongomk.api.instruction.Instruction.SetPropertyInstruction; +import org.apache.jackrabbit.mongomk.api.model.Commit; +import org.apache.jackrabbit.mongomk.impl.InstructionAssert; +import org.apache.jackrabbit.mongomk.impl.model.CommitBuilder; +import org.junit.Test; + +public class CommitBuilderTest { + + private static final String MESSAGE = "This is a simple commit"; + private static final String ROOT = "/"; + + @Test + public void testSimpleAdd() throws Exception { + StringBuilder sb = new StringBuilder(); + sb.append("+\"a\" : { \"int\" : 1 } \n"); + sb.append("+\"a/b\" : { \"string\" : \"foo\" } \n"); + sb.append("+\"a/c\" : { \"bool\" : true }"); + + Commit commit = buildAndAssertCommit(sb.toString()); + + List instructions = commit.getInstructions(); + Assert.assertEquals(6, instructions.size()); + InstructionAssert.assertAddNodeInstruction((AddNodeInstruction) instructions.get(0), "/a"); + InstructionAssert.assertSetPropertyInstruction((SetPropertyInstruction) instructions.get(1), "/a", "int", 1); + InstructionAssert.assertAddNodeInstruction((AddNodeInstruction) instructions.get(2), "/a/b"); + InstructionAssert.assertSetPropertyInstruction((SetPropertyInstruction) instructions.get(3), "/a/b", "string", + "foo"); + InstructionAssert.assertAddNodeInstruction((AddNodeInstruction) instructions.get(4), "/a/c"); + InstructionAssert.assertSetPropertyInstruction((SetPropertyInstruction) instructions.get(5), "/a/c", "bool", + true); + } + + @Test + public void testSimpleCopy() throws Exception { + StringBuilder sb = new StringBuilder(); + sb.append("*\"a\" : \"b\"\n"); + sb.append("*\"a/b\" : \"a/c\"\n"); + + Commit commit = buildAndAssertCommit(sb.toString()); + List instructions = commit.getInstructions(); + assertEquals(2, instructions.size()); + InstructionAssert.assertCopyNodeInstruction((CopyNodeInstruction) instructions.get(0), "/", "/a", "/b"); + InstructionAssert.assertCopyNodeInstruction((CopyNodeInstruction) instructions.get(1), "/", "/a/b", "/a/c"); + } + + @Test + public void testSimpleMove() throws Exception { + StringBuilder sb = new StringBuilder(); + sb.append(">\"a\" : \"b\"\n"); + sb.append(">\"a/b\" : \"a/c\"\n"); + + Commit commit = buildAndAssertCommit(sb.toString()); + List instructions = commit.getInstructions(); + assertEquals(2, instructions.size()); + InstructionAssert.assertMoveNodeInstruction((MoveNodeInstruction) instructions.get(0), "/", "/a", "/b"); + InstructionAssert.assertMoveNodeInstruction((MoveNodeInstruction) instructions.get(1), "/", "/a/b", "/a/c"); + } + + @Test + public void testSimpleRemove() throws Exception { + StringBuilder sb = new StringBuilder(); + sb.append("-\"a\""); + sb.append("^\"a/prop\" : null"); + + Commit commit = buildAndAssertCommit(sb.toString()); + List instructions = commit.getInstructions(); + assertEquals(2, instructions.size()); + InstructionAssert.assertRemoveNodeInstruction((RemoveNodeInstruction) instructions.get(0), + "/a"); + InstructionAssert.assertSetPropertyInstruction((SetPropertyInstruction)instructions.get(1), + "/a", "prop", null); + } + + @Test + public void testSimpleSet() throws Exception { + StringBuilder sb = new StringBuilder(); + sb.append("^\"a\" : \"b\"\n"); + + Commit commit = buildAndAssertCommit(sb.toString()); + List instructions = commit.getInstructions(); + assertEquals(1, instructions.size()); + InstructionAssert.assertSetPropertyInstruction((SetPropertyInstruction) instructions.get(0), "/", "a", "b"); + } + + private Commit buildAndAssertCommit(String commitString) throws Exception { + Commit commit = CommitBuilder.build(ROOT, commitString, MESSAGE); + assertNotNull(commit); + assertEquals(MESSAGE, commit.getMessage()); + assertNull(commit.getRevisionId()); + return commit; + } +} diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/builder/NodeBuilder.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/builder/NodeBuilder.java new file mode 100644 index 00000000000..18b9bdaec0a --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/builder/NodeBuilder.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl.builder; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.apache.jackrabbit.mongomk.api.model.Node; +import org.apache.jackrabbit.mongomk.impl.json.JsonUtil; +import org.apache.jackrabbit.mongomk.impl.model.NodeImpl; +import org.apache.jackrabbit.mongomk.util.MongoUtil; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * A builder to create {@link Node}s from JSON + * strings. + */ +public class NodeBuilder { + + /** + * Creates {@link Node} from the given {@code json} and an empty path as root path. + * + * @param json The {@code json}. + * @return The {@code Node}. + * @throws Exception If an error occurred while creating. + * @see #build(String, String) + */ + public static Node build(String json) throws Exception { + return build(json, ""); + } + + /** + * Creates {@link Node} from the given {@code json} and an empty path as root path. + * + * @param json The {@code json}. + * @param path The root path of the nodes. + * @return The {@code Node}. + * @throws Exception If an error occurred while creating. + * @see #build(String, String) + */ + public static Node build(String json, String path) throws Exception { + NodeBuilder nodeBuilder = new NodeBuilder(); + + return nodeBuilder.doBuild(json, path); + } + + private NodeBuilder() { + // only private construction + } + + private Node doBuild(String json, String path) throws Exception { + try { + JSONObject jsonObject = new JSONObject(json); + JSONArray names = jsonObject.names(); + if (names.length() != 1) { + throw new IllegalArgumentException("JSON must contain exactly 1 root node"); + } + + String name = names.getString(0); + JSONObject value = jsonObject.getJSONObject(name); + + return parseNode(PathUtils.concat(path, name), value); + } catch (JSONException e) { + throw new Exception(e); + } + } + + private Node parseNode(String path, JSONObject jsonObject) throws Exception { + String realPath = path; + String revisionId = null; + + int index = path.lastIndexOf('#'); + if (index != -1) { + realPath = path.substring(0, index); + revisionId = path.substring(index + 1); + } + + NodeImpl node = new NodeImpl(realPath); + node.setRevisionId(MongoUtil.toMongoRepresentation(revisionId)); + + Map properties = null; + for (@SuppressWarnings("rawtypes") + Iterator iterator = jsonObject.keys(); iterator.hasNext();) { + String key = (String) iterator.next(); + Object value = jsonObject.get(key); + + if (value instanceof JSONObject) { + String childPath = PathUtils.concat(realPath, key); + + Node childNode = parseNode(childPath, (JSONObject) value); + node.addChildNodeEntry(childNode); + } else { + if (properties == null) { + properties = new HashMap(); + } + + Object converted = JsonUtil.toJsonValue(value.toString()); + properties.put(key, converted); + } + } + + if (properties != null) { + for (Map.Entry entry : properties.entrySet()) { + node.addProperty(entry.getKey(), entry.getValue()); + } + } + + return node; + } +} diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/builder/NodeBuilderTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/builder/NodeBuilderTest.java new file mode 100644 index 00000000000..c3eadef6e3a --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/builder/NodeBuilderTest.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.mongomk.impl.builder; + +import org.apache.jackrabbit.mongomk.api.model.Node; +import org.apache.jackrabbit.mongomk.impl.NodeAssert; +import org.apache.jackrabbit.mongomk.impl.builder.NodeBuilder; +import org.apache.jackrabbit.mongomk.impl.model.NodeImpl; +import org.junit.Test; + +public class NodeBuilderTest { + + @Test + public void testBuildSimpleNodes() throws Exception { + String json = "{ \"/\" : { \"a\" : { \"b\" : {} , \"c\" : {} } } }"; + Node node = NodeBuilder.build(json); + + NodeImpl node_c = new NodeImpl("/a/c"); + NodeImpl node_b = new NodeImpl("/a/b"); + NodeImpl node_a = new NodeImpl("/a"); + node_a.addChildNodeEntry(node_b); + node_a.addChildNodeEntry(node_c); + NodeImpl node_root = new NodeImpl("/"); + node_root.addChildNodeEntry(node_a); + + NodeAssert.assertDeepEquals(node, node_root); + } + + @Test + public void testBuildSimpleNodesWithRevisionId() throws Exception { + String json = "{ \"/#1\" : { \"a#1\" : { \"b#2\" : {} , \"c#2\" : {} } } }"; + Node node = NodeBuilder.build(json); + + NodeImpl node_c = new NodeImpl("/a/c"); + node_c.setRevisionId(2L); + + NodeImpl node_b = new NodeImpl("/a/b"); + node_b.setRevisionId(2L); + + NodeImpl node_a = new NodeImpl("/a"); + node_a.addChildNodeEntry(node_b); + node_a.addChildNodeEntry(node_c); + node_a.setRevisionId(1L); + + NodeImpl node_root = new NodeImpl("/"); + node_root.addChildNodeEntry(node_a); + node_root.setRevisionId(1L); + + NodeAssert.assertDeepEquals(node, node_root); + } +} diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/command/CommitCommandTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/command/CommitCommandTest.java new file mode 100644 index 00000000000..88ff5e17631 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/command/CommitCommandTest.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.mongomk.impl.command; + +import org.apache.jackrabbit.mongomk.BaseMongoMicroKernelTest; +import org.apache.jackrabbit.mongomk.MongoAssert; +import org.apache.jackrabbit.mongomk.api.model.Commit; +import org.apache.jackrabbit.mongomk.impl.SimpleNodeScenario; +import org.apache.jackrabbit.mongomk.impl.builder.NodeBuilder; +import org.apache.jackrabbit.mongomk.impl.command.CommitCommand; +import org.apache.jackrabbit.mongomk.impl.model.CommitBuilder; +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests for {@code CommitCommandMongo} + */ +public class CommitCommandTest extends BaseMongoMicroKernelTest { + + @Test + public void initialCommit() throws Exception { + Commit commit = CommitBuilder.build("/", "+\"a\" : { \"b\" : {} , \"c\" : {} }", null); + CommitCommand command = new CommitCommand(mongoConnection, commit); + Long revisionId = command.execute(); + + Assert.assertNotNull(revisionId); + MongoAssert.assertNodesExist(NodeBuilder.build(String.format( + "{ \"/#%1$s\" : { \"a#%1$s\" : { \"b#%1$s\" : {} , \"c#%1$s\" : {} } } }", revisionId))); + + MongoAssert.assertCommitExists(commit); + MongoAssert.assertCommitContainsAffectedPaths(commit.getRevisionId().toString(), + "/", "/a", "/a/b", "/a/c"); + MongoAssert.assertHeadRevision(1); + MongoAssert.assertNextRevision(2); + } + + @Test + public void ontainsAllAffectedNodes() throws Exception { + SimpleNodeScenario scenario = new SimpleNodeScenario(mk); + String rev1 = scenario.create(); + String rev2 = scenario.update_A_and_add_D_and_E(); + MongoAssert.assertCommitContainsAffectedPaths(rev1, "/", "/a", "/a/b", "/a/c"); + MongoAssert.assertCommitContainsAffectedPaths(rev2, "/a", "/a/b", "/a/d", "/a/b/e"); + } + + @Test + public void noOtherNodesTouched() throws Exception { + String rev1 = mk.commit("/", "+\"a\" : {} +\"b\" : {} +\"c\" : {}", null, null); + String rev2 = mk.commit("/a", "+\"d\": {} +\"e\" : {}", null, null); + + MongoAssert.assertNodeRevisionId("/", rev1, true); + MongoAssert.assertNodeRevisionId("/a", rev1, true); + MongoAssert.assertNodeRevisionId("/b", rev1, true); + MongoAssert.assertNodeRevisionId("/c", rev1, true); + MongoAssert.assertNodeRevisionId("/a/d", rev1, false); + MongoAssert.assertNodeRevisionId("/a/e", rev1, false); + + MongoAssert.assertNodeRevisionId("/", rev2, false); + MongoAssert.assertNodeRevisionId("/a", rev2, true); + MongoAssert.assertNodeRevisionId("/b", rev2, false); + MongoAssert.assertNodeRevisionId("/c", rev2, false); + MongoAssert.assertNodeRevisionId("/a/d", rev2, true); + MongoAssert.assertNodeRevisionId("/a/e", rev2, true); + } +} \ No newline at end of file diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/command/ConcurrentCommitCommandTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/command/ConcurrentCommitCommandTest.java new file mode 100644 index 00000000000..ee2a3db3982 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/command/ConcurrentCommitCommandTest.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl.command; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.apache.jackrabbit.mongomk.BaseMongoTest; +import org.apache.jackrabbit.mongomk.action.FetchCommitsAction; +import org.apache.jackrabbit.mongomk.api.command.CommandExecutor; +import org.apache.jackrabbit.mongomk.api.model.Commit; +import org.apache.jackrabbit.mongomk.api.model.Node; +import org.apache.jackrabbit.mongomk.impl.command.CommitCommand; +import org.apache.jackrabbit.mongomk.impl.command.DefaultCommandExecutor; +import org.apache.jackrabbit.mongomk.impl.command.GetNodesCommand; +import org.apache.jackrabbit.mongomk.impl.model.CommitBuilder; +import org.apache.jackrabbit.mongomk.model.CommitMongo; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.junit.Test; + +public class ConcurrentCommitCommandTest extends BaseMongoTest { + + @Test + public void testConflictingConcurrentUpdate() throws Exception { + int numOfConcurrentThreads = 5; + final Object waitLock = new Object(); + + // create the commands + List commands = new ArrayList(numOfConcurrentThreads); + for (int i = 0; i < numOfConcurrentThreads; ++i) { + Commit commit = CommitBuilder.build("/", "+\"" + i + "\" : {}", + "This is a concurrent commit"); + CommitCommand command = new CommitCommand(mongoConnection, commit) { + @Override + protected boolean saveAndSetHeadRevision() throws Exception { + try { + synchronized (waitLock) { + waitLock.wait(); + } + + return super.saveAndSetHeadRevision(); + } catch (InterruptedException e) { + e.printStackTrace(); + return false; + } + }; + }; + commands.add(command); + } + + // execute the commands + final CommandExecutor commandExecutor = new DefaultCommandExecutor(); + ExecutorService executorService = Executors.newFixedThreadPool(numOfConcurrentThreads); + final List revisionIds = new LinkedList(); + for (int i = 0; i < numOfConcurrentThreads; ++i) { + final CommitCommand command = commands.get(i); + Runnable runnable = new Runnable() { + + @Override + public void run() { + try { + Long revisionId = commandExecutor.execute(command); + revisionIds.add(revisionId); + } catch (Exception e) { + revisionIds.add(null); + } + } + }; + executorService.execute(runnable); + } + + // notify the wait lock to execute the command concurrently + do { + Thread.sleep(1500); + synchronized (waitLock) { + waitLock.notifyAll(); + } + } while (revisionIds.size() < numOfConcurrentThreads); + + // Verify the result by sorting the revision ids and verifying that all + // children are contained in the next revision + Collections.sort(revisionIds, new Comparator() { + @Override + public int compare(Long o1, Long o2) { + return o1.compareTo(o2); + } + }); + List lastChildren = new LinkedList(); + for (int i = 0; i < numOfConcurrentThreads; ++i) { + Long revisionId = revisionIds.get(i); + GetNodesCommand command2 = new GetNodesCommand(mongoConnection, + "/", revisionId); + Node root = command2.execute(); + + for (String lastChild : lastChildren) { + boolean contained = false; + for (Iterator it = root.getChildNodeEntries(0, -1); it.hasNext(); ) { + Node childNode = it.next(); + String childName = PathUtils.getName(childNode.getPath()); + if (childName.equals(lastChild)) { + contained = true; + break; + } + } + assertTrue(contained); + } + lastChildren.clear(); + for (Iterator it = root.getChildNodeEntries(0, -1); it.hasNext(); ) { + Node childNode = it.next(); + String childName = PathUtils.getName(childNode.getPath()); + lastChildren.add(childName); + } + } + + // Assert number of successful commits. + List commits = new FetchCommitsAction(mongoConnection).execute(); + assertEquals(numOfConcurrentThreads + 1, commits.size()); + } +} diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/command/ConcurrentWriteMultipleMkMongoTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/command/ConcurrentWriteMultipleMkMongoTest.java new file mode 100644 index 00000000000..c6b592d0514 --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/command/ConcurrentWriteMultipleMkMongoTest.java @@ -0,0 +1,133 @@ +package org.apache.jackrabbit.mongomk.impl.command; + +import java.io.InputStream; +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mongomk.BaseMongoTest; +import org.apache.jackrabbit.mongomk.impl.BlobStoreMongo; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.impl.MongoMicroKernel; +import org.apache.jackrabbit.mongomk.impl.NodeStoreMongo; + +import org.junit.Test; + +public class ConcurrentWriteMultipleMkMongoTest extends BaseMongoTest { + + @Test + public void testConcurrency() throws NumberFormatException, Exception { + + String diff1 = buildPyramidDiff("/", 0, 10, 100, "N", + new StringBuilder()).toString(); + String diff2 = buildPyramidDiff("/", 0, 10, 100, "P", + new StringBuilder()).toString(); + String diff3 = buildPyramidDiff("/", 0, 10, 100, "R", + new StringBuilder()).toString(); + + // System.out.println(diff1); + // System.out.println(diff2); + // System.out.println(diff3); + + InputStream is = BaseMongoTest.class.getResourceAsStream("/config.cfg"); + Properties properties = new Properties(); + properties.load(is); + + String host = properties.getProperty("host"); + int port = Integer.parseInt(properties.getProperty("port")); + String db = properties.getProperty("db"); + + MongoMicroKernel mongo1 = new MongoMicroKernel(new NodeStoreMongo( + mongoConnection), new BlobStoreMongo(mongoConnection)); + MongoMicroKernel mongo2 = new MongoMicroKernel(new NodeStoreMongo( + mongoConnection), new BlobStoreMongo(mongoConnection)); + MongoMicroKernel mongo3 = new MongoMicroKernel(new NodeStoreMongo( + mongoConnection), new BlobStoreMongo(mongoConnection)); + + GenericWriteTask task1 = new GenericWriteTask(mongo1, diff1, 0, + new MongoConnection(host, port, db)); + GenericWriteTask task2 = new GenericWriteTask(mongo2, diff2, 0, + new MongoConnection(host, port, db)); + GenericWriteTask task3 = new GenericWriteTask(mongo3, diff3, 0, + new MongoConnection(host, port, db)); + + ExecutorService threadExecutor = Executors.newFixedThreadPool(3); + threadExecutor.execute(task1); + threadExecutor.execute(task2); + threadExecutor.execute(task3); + threadExecutor.shutdown(); + threadExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + } + + private static StringBuilder buildPyramidDiff(String startingPoint, + int index, int numberOfChildren, long nodesNumber, + String nodePrefixName, StringBuilder diff) { + if (numberOfChildren == 0) { + for (long i = 0; i < nodesNumber; i++) + diff.append(addNodeToDiff(startingPoint, nodePrefixName + i)); + return diff; + } + if (index >= nodesNumber) + return diff; + diff.append(addNodeToDiff(startingPoint, nodePrefixName + index)); + for (int i = 1; i <= numberOfChildren; i++) { + if (!startingPoint.endsWith("/")) + startingPoint = startingPoint + "/"; + buildPyramidDiff(startingPoint + nodePrefixName + index, index + * numberOfChildren + i, numberOfChildren, nodesNumber, + nodePrefixName, diff); + } + return diff; + } + + private static String addNodeToDiff(String startingPoint, String nodeName) { + if (!startingPoint.endsWith("/")) + startingPoint = startingPoint + "/"; + + return ("+\"" + startingPoint + nodeName + "\" : {\"key\":\"00000000000000000000\"} \n"); + } +} + +class GenericWriteTask implements Runnable { + + MicroKernel mk; + String diff; + int nodesPerCommit; + + public GenericWriteTask(MongoMicroKernel mk, String diff, + int nodesPerCommit, MongoConnection mongoConnection) { + + this.diff = diff; + this.mk = mk; + } + + @Override + public void run() { + commit(mk, diff, 10); + } + + private void commit(MicroKernel mk, String diff, int nodesPerCommit) { + + if (nodesPerCommit == 0) { + mk.commit("", diff.toString(), null, ""); + return; + } + String[] string = diff.split(System.getProperty("line.separator")); + int i = 0; + StringBuilder finalCommit = new StringBuilder(); + for (String line : string) { + finalCommit.append(line); + i++; + if (i == nodesPerCommit) { + mk.commit("", finalCommit.toString(), null, ""); + finalCommit.setLength(0); + i = 0; + } + } + // commit remaining nodes + if (finalCommit.length() > 0) + mk.commit("", finalCommit.toString(), null, ""); + } +} diff --git a/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/json/JsopParserTest.java b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/json/JsopParserTest.java new file mode 100644 index 00000000000..64c8ca8a08f --- /dev/null +++ b/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/impl/json/JsopParserTest.java @@ -0,0 +1,517 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.mongomk.impl.json; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import org.apache.jackrabbit.mongomk.impl.json.DefaultJsopHandler; +import org.apache.jackrabbit.mongomk.impl.json.JsopParser; +import org.junit.Assert; +import org.junit.Test; + +public class JsopParserTest { + + private static class CountingHandler extends DefaultJsopHandler { + + private static class Node { + private final String jsop; + private final String path; + + Node(String jsop, String path) { + this.jsop = jsop; + this.path = path; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (this.getClass() != obj.getClass()) { + return false; + } + Node other = (Node) obj; + if (this.jsop == null) { + if (other.jsop != null) { + return false; + } + } else if (!this.jsop.equals(other.jsop)) { + return false; + } + if (this.path == null) { + if (other.path != null) { + return false; + } + } else if (!this.path.equals(other.path)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = (prime * result) + ((this.jsop == null) ? 0 : this.jsop.hashCode()); + result = (prime * result) + ((this.path == null) ? 0 : this.path.hashCode()); + return result; + } + } + + private static class NodeMoved { + private final String newPath; + private final String oldPath; + private final String rootPath; + + NodeMoved(String rootPath, String oldPath, String newPath) { + this.rootPath = rootPath; + this.oldPath = oldPath; + this.newPath = newPath; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (this.getClass() != obj.getClass()) { + return false; + } + NodeMoved other = (NodeMoved) obj; + if (this.newPath == null) { + if (other.newPath != null) { + return false; + } + } else if (!this.newPath.equals(other.newPath)) { + return false; + } + if (this.oldPath == null) { + if (other.oldPath != null) { + return false; + } + } else if (!this.oldPath.equals(other.oldPath)) { + return false; + } + if (this.rootPath == null) { + if (other.rootPath != null) { + return false; + } + } else if (!this.rootPath.equals(other.rootPath)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = (prime * result) + ((this.newPath == null) ? 0 : this.newPath.hashCode()); + result = (prime * result) + ((this.oldPath == null) ? 0 : this.oldPath.hashCode()); + result = (prime * result) + ((this.rootPath == null) ? 0 : this.rootPath.hashCode()); + return result; + } + + } + + private static class Property { + private final String key; + private final String path; + private final Object value; + + Property(String path, String key, Object value) { + this.path = path; + this.key = key; + this.value = value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (this.getClass() != obj.getClass()) { + return false; + } + Property other = (Property) obj; + if (this.key == null) { + if (other.key != null) { + return false; + } + } else if (!this.key.equals(other.key)) { + return false; + } + if (this.path == null) { + if (other.path != null) { + return false; + } + } else if (!this.path.equals(other.path)) { + return false; + } + if (this.value == null) { + if (other.value != null) { + return false; + } + } else if (this.value instanceof Object[]) { + return Arrays.deepEquals((Object[]) this.value, (Object[]) other.value); + } else if (!this.value.equals(other.value)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = (prime * result) + ((this.key == null) ? 0 : this.key.hashCode()); + result = (prime * result) + ((this.path == null) ? 0 : this.path.hashCode()); + result = (prime * result) + ((this.value == null) ? 0 : this.value.hashCode()); + return result; + } + } + + private final List nodesAdded; + private final List nodesCopied; + private final List nodesMoved; + private final List nodesRemoved; + private final List propertiesSet; + + CountingHandler() { + this.nodesAdded = new LinkedList(); + this.nodesCopied = new LinkedList(); + this.nodesMoved = new LinkedList(); + this.nodesRemoved = new LinkedList(); + this.propertiesSet = new LinkedList(); + } + + public void assertNodeCopied(String parentPath, String oldPath, String newPath) { + NodeMoved expected = new NodeMoved(parentPath, oldPath, newPath); + + int firstIndex = this.nodesCopied.indexOf(expected); + int lastIndex = this.nodesCopied.lastIndexOf(expected); + + Assert.assertTrue(firstIndex != -1); + Assert.assertEquals(firstIndex, lastIndex); + } + + public void assertNodeMoved(String parentPath, String oldPath, String newPath) { + NodeMoved expected = new NodeMoved(parentPath, oldPath, newPath); + + int firstIndex = this.nodesMoved.indexOf(expected); + int lastIndex = this.nodesMoved.lastIndexOf(expected); + + Assert.assertTrue(firstIndex != -1); + Assert.assertEquals(firstIndex, lastIndex); + } + + public void assertNodeRemoved(String path, String name) { + Node expected = new Node(path, name); + + int firstIndex = this.nodesRemoved.indexOf(expected); + int lastIndex = this.nodesRemoved.lastIndexOf(expected); + + Assert.assertTrue(firstIndex != -1); + Assert.assertEquals(firstIndex, lastIndex); + } + + public void assertNoOfNodesCopied(int num) { + Assert.assertEquals(num, this.nodesCopied.size()); + } + + public void assertNoOfNodesMoved(int num) { + Assert.assertEquals(num, this.nodesMoved.size()); + } + + public void assertNoOfNodesRemoved(int num) { + Assert.assertEquals(num, this.nodesRemoved.size()); + } + + public void assertNoOfPropertiesSet(int num) { + Assert.assertEquals(num, this.propertiesSet.size()); + } + + @Override + public void nodeAdded(String path, String name) { + this.nodesAdded.add(new Node(path, name)); + } + + @Override + public void nodeCopied(String rootPath, String oldPath, String newPath) { + this.nodesCopied.add(new NodeMoved(rootPath, oldPath, newPath)); + } + + @Override + public void nodeMoved(String rootPath, String oldPath, String newPath) { + this.nodesMoved.add(new NodeMoved(rootPath, oldPath, newPath)); + } + + @Override + public void nodeRemoved(String path, String name) { + this.nodesRemoved.add(new Node(path, name)); + } + + @Override + public void propertySet(String path, String key, Object value) { + this.propertiesSet.add(new Property(path, key, value)); + } + + void assertNodeAdded(String path, String name) { + Node expected = new Node(path, name); + + int firstIndex = this.nodesAdded.indexOf(expected); + int lastIndex = this.nodesAdded.lastIndexOf(expected); + + Assert.assertTrue(firstIndex != -1); + Assert.assertEquals(firstIndex, lastIndex); + } + + void assertPropertySet(String path, String key, Object value) { + Property expected = new Property(path, key, value); + + int firstIndex = this.propertiesSet.indexOf(expected); + int lastIndex = this.propertiesSet.lastIndexOf(expected); + + Assert.assertTrue(firstIndex != -1); + Assert.assertEquals(firstIndex, lastIndex); + } + + void assetNoOfNodesAdded(int num) { + Assert.assertEquals(num, this.nodesAdded.size()); + } + + } + + @Test + public void testAddNestedNodes() throws Exception { + String rootPath = "/"; + StringBuilder sb = new StringBuilder(); + sb.append("+\"a\" : { \"integer\" : 123 ,\"b\" : { \"double\" : 123.456 , \"d\" : {} } , \"c\" : { \"string\" : \"string\" }}"); + + CountingHandler countingHandler = new CountingHandler(); + JsopParser jsopParser = new JsopParser(rootPath, sb.toString(), countingHandler); + + jsopParser.parse(); + + countingHandler.assetNoOfNodesAdded(4); + countingHandler.assertNodeAdded("/", "a"); + countingHandler.assertNodeAdded("/a", "b"); + countingHandler.assertNodeAdded("/a/b", "d"); + countingHandler.assertNodeAdded("/a", "c"); + + countingHandler.assertNoOfPropertiesSet(3); + countingHandler.assertPropertySet("/a", "integer", 123); + countingHandler.assertPropertySet("/a/b", "double", 123.456); + countingHandler.assertPropertySet("/a/c", "string", "string"); + } + + @Test + public void testAddNodesAndProperties() throws Exception { + String rootPath = "/"; + StringBuilder sb = new StringBuilder(); + sb.append("+\"a\" : { \"int\" : 1 } \n"); + sb.append("+\"a/b\" : { \"string\" : \"foo\" } \n"); + sb.append("+\"a/c\" : { \"bool\" : true }"); + + CountingHandler countingHandler = new CountingHandler(); + JsopParser jsopParser = new JsopParser(rootPath, sb.toString(), countingHandler); + + jsopParser.parse(); + + countingHandler.assetNoOfNodesAdded(3); + countingHandler.assertNodeAdded("/", "a"); + countingHandler.assertNodeAdded("/a", "b"); + countingHandler.assertNodeAdded("/a", "c"); + + countingHandler.assertNoOfPropertiesSet(3); + countingHandler.assertPropertySet("/a", "int", Integer.valueOf(1)); + countingHandler.assertPropertySet("/a/b", "string", "foo"); + countingHandler.assertPropertySet("/a/c", "bool", Boolean.TRUE); + } + + @Test + public void testAddNodesAndPropertiesSeparately() throws Exception { + String rootPath = "/"; + StringBuilder sb = new StringBuilder(); + sb.append("+\"a\" : {} \n"); + sb.append("+\"a\" : { \"int\" : 1 } \n"); + sb.append("+\"a/b\" : {} \n"); + sb.append("+\"a/b\" : { \"string\" : \"foo\" } \n"); + sb.append("+\"a/c\" : {} \n"); + sb.append("+\"a/c\" : { \"bool\" : true }"); + + CountingHandler countingHandler = new CountingHandler(); + JsopParser jsopParser = new JsopParser(rootPath, sb.toString(), countingHandler); + + jsopParser.parse(); + + countingHandler.assetNoOfNodesAdded(6); + + countingHandler.assertNoOfPropertiesSet(3); + countingHandler.assertPropertySet("/a", "int", Integer.valueOf(1)); + countingHandler.assertPropertySet("/a/b", "string", "foo"); + countingHandler.assertPropertySet("/a/c", "bool", Boolean.TRUE); + } + + @Test + public void testAddPropertiesWithComplexArray() throws Exception { + String rootPath = "/"; + String jsop = "+ \"a\" : { \"array_complex\" : [ 123, 123.456, true, false, null, \"string\", [1,2,3,4,5] ] }"; + + CountingHandler countingHandler = new CountingHandler(); + JsopParser jsopParser = new JsopParser(rootPath, jsop, countingHandler); + + jsopParser.parse(); + + countingHandler.assertNoOfPropertiesSet(1); + countingHandler.assertPropertySet( + "/a", + "array_complex", + Arrays.asList(new Object[] { 123, 123.456, true, false, null, "string", + Arrays.asList(new Object[] { 1, 2, 3, 4, 5 }) })); + } + + @Test + public void testAddWithEmptyPath() throws Exception { + String rootPath = ""; + StringBuilder sb = new StringBuilder(); + sb.append("+\"/\" : { \"int\" : 1 } \n"); + + CountingHandler countingHandler = new CountingHandler(); + JsopParser jsopParser = new JsopParser(rootPath, sb.toString(), countingHandler); + + jsopParser.parse(); + + countingHandler.assetNoOfNodesAdded(1); + countingHandler.assertNodeAdded("", "/"); + + countingHandler.assertNoOfPropertiesSet(1); + countingHandler.assertPropertySet("/", "int", Integer.valueOf(1)); + } + + @Test + public void testSimpleAddNodes() throws Exception { + String rootPath = "/"; + StringBuilder sb = new StringBuilder(); + sb.append("+\"a\" : {} \n"); + sb.append("+\"a/b\" : {} \n"); + sb.append("+\"a/c\" : {}"); + + CountingHandler countingHandler = new CountingHandler(); + JsopParser jsopParser = new JsopParser(rootPath, sb.toString(), countingHandler); + + jsopParser.parse(); + + countingHandler.assetNoOfNodesAdded(3); + countingHandler.assertNodeAdded("/", "a"); + countingHandler.assertNodeAdded("/a", "b"); + countingHandler.assertNodeAdded("/a", "c"); + } + + @Test + public void testSimpleAddProperties() throws Exception { + String rootPath = "/"; + StringBuilder sb = new StringBuilder(); + sb.append("+ \"a\" : {}"); + sb.append("+ \"a\" : { \"integer\" : 123, \"double\" : 123.456, \"true\" : true, \"false\" : false, \"null\" : null, \"string\" : \"string\", \"array\" : [1,2,3,4,5] }"); + + CountingHandler countingHandler = new CountingHandler(); + JsopParser jsopParser = new JsopParser(rootPath, sb.toString(), countingHandler); + + jsopParser.parse(); + + countingHandler.assertNoOfPropertiesSet(7); + countingHandler.assertPropertySet("/a", "integer", 123); + countingHandler.assertPropertySet("/a", "double", 123.456); + countingHandler.assertPropertySet("/a", "true", true); + countingHandler.assertPropertySet("/a", "false", false); + countingHandler.assertPropertySet("/a", "null", null); + countingHandler.assertPropertySet("/a", "string", "string"); + countingHandler.assertPropertySet("/a", "array", Arrays.asList(new Object[] { 1, 2, 3, 4, 5 })); + } + + @Test + public void testSimpleCopyNodes() throws Exception { + String rootPath = "/"; + StringBuilder sb = new StringBuilder(); + sb.append("*\"a\" : \"b\"\n"); + sb.append("*\"a/b\" : \"a/c\"\n"); + + CountingHandler countingHandler = new CountingHandler(); + JsopParser jsopParser = new JsopParser(rootPath, sb.toString(), countingHandler); + jsopParser.parse(); + + countingHandler.assertNoOfNodesCopied(2); + countingHandler.assertNodeCopied("/", "/a", "/b"); + countingHandler.assertNodeCopied("/", "/a/b", "/a/c"); + } + + @Test + public void testSimpleMoveNodes() throws Exception { + String rootPath = "/"; + StringBuilder sb = new StringBuilder(); + sb.append(">\"a\" : \"b\"\n"); + sb.append(">\"a/b\" : \"a/c\"\n"); + + CountingHandler countingHandler = new CountingHandler(); + JsopParser jsopParser = new JsopParser(rootPath, sb.toString(), countingHandler); + jsopParser.parse(); + + countingHandler.assertNoOfNodesMoved(2); + countingHandler.assertNodeMoved("/", "/a", "/b"); + countingHandler.assertNodeMoved("/", "/a/b", "/a/c"); + } + + @Test + public void testSimpleRemoveNodes() throws Exception { + String rootPath = "/"; + String jsop = "-\"a\""; + + CountingHandler countingHandler = new CountingHandler(); + JsopParser jsopParser = new JsopParser(rootPath, jsop, countingHandler); + + jsopParser.parse(); + + countingHandler.assertNoOfNodesRemoved(1); + countingHandler.assertNodeRemoved("/", "a"); + } + + @Test + public void testSimpleSetNodes() throws Exception { + String rootPath = "/"; + StringBuilder sb = new StringBuilder(); + sb.append("^\"a\" : \"b\""); + + CountingHandler countingHandler = new CountingHandler(); + JsopParser jsopParser = new JsopParser(rootPath, sb.toString(), countingHandler); + jsopParser.parse(); + + countingHandler.assertNoOfPropertiesSet(1); + countingHandler.assertPropertySet("/", "a", "b"); + } +} diff --git a/oak-mongomk/src/test/resources/config.cfg b/oak-mongomk/src/test/resources/config.cfg new file mode 100644 index 00000000000..39ce31bdfe2 --- /dev/null +++ b/oak-mongomk/src/test/resources/config.cfg @@ -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. + +# This file contains configuration properties for the MongoDB related services. These properties are +# required for the SharedCloud MicroKernel integration tests. +# +# Please be cautious with any blanklines and whitespaces! + +# The host of the running mongodb or mongos process +host = 127.0.0.1 + +# The port of the running mongodb or mongos process +port = 27017 + +# The database to use +db = MongoMicroKernelTest \ No newline at end of file diff --git a/oak-mongomk/src/test/resources/logback-test.xml b/oak-mongomk/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..e92401c93ea --- /dev/null +++ b/oak-mongomk/src/test/resources/logback-test.xml @@ -0,0 +1,39 @@ + + + + + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + target/unit-tests.log + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + + + + + diff --git a/oak-parent/pom.xml b/oak-parent/pom.xml index 778050c3edf..0e59bcad227 100644 --- a/oak-parent/pom.xml +++ b/oak-parent/pom.xml @@ -17,27 +17,31 @@ limitations under the License. --> - + 4.0.0 org.apache apache 10 - + org.apache.jackrabbit oak-parent Oak Parent POM - 0.1-SNAPSHOT + 0.6-SNAPSHOT pom + -Xmx512m -XX:MaxPermSize=32m false + + + ${project.build.sourceEncoding} + + 2.5.2 + 13.0.1 @@ -73,18 +77,116 @@ + + org.apache.felix + maven-bundle-plugin + 2.3.7 + true + + + org.apache.felix + maven-scr-plugin + 1.7.4 + + + generate-scr-scrdescriptor + + scr + + + + maven-deploy-plugin ${skip.deployment} + + org.apache.rat + apache-rat-plugin + 0.8 + + + maven-surefire-plugin + + ${test.opts} + + ${known.issues} + + + + + maven-failsafe-plugin + 2.12 + + ${test.opts} + + ${known.issues} + + + + + maven-checkstyle-plugin + 2.9.1 + + + org.codehaus.mojo + findbugs-maven-plugin + 2.5.1 + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + org.apache.felix + maven-scr-plugin + [1.7.4,) + + scr + + + + + + + + + + + + org.osgi + org.osgi.core + 4.2.0 + + + org.osgi + org.osgi.compendium + 4.2.0 + + + biz.aQute + bndlib + 1.50.0 + + + org.apache.felix + org.apache.felix.scr.annotations + 1.6.0 + junit junit @@ -93,4 +195,75 @@ - \ No newline at end of file + + + integrationTesting + + + env.OAK_INTEGRATION_TESTING + + + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + + + + + + pedantic + + + + org.apache.rat + apache-rat-plugin + + + verify + + check + + + + + + maven-checkstyle-plugin + + false + + + + + check + + + + + + org.codehaus.mojo + findbugs-maven-plugin + + false + + + + + check + + + + + + + + + diff --git a/oak-run/pom.xml b/oak-run/pom.xml index da48ffb4f1e..841694ef79b 100644 --- a/oak-run/pom.xml +++ b/oak-run/pom.xml @@ -17,16 +17,13 @@ limitations under the License. --> - + 4.0.0 org.apache.jackrabbit oak-parent - 0.1-SNAPSHOT + 0.6-SNAPSHOT ../oak-parent/pom.xml @@ -35,6 +32,142 @@ true + 8.1.2.v20120308 + + + + org.apache.maven.plugins + maven-shade-plugin + 1.6 + + + package + + shade + + + false + + + * + + + + + * + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + org.apache.jackrabbit.oak.run.Main + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 1.7 + + + reserve-network-port + + reserve-network-port + + process-resources + + + jetty.http.port + + + + + + + maven-surefire-plugin + + + ${jetty.http.port} + + + + + + + + + org.apache.jackrabbit + oak-jcr + ${project.version} + + + org.apache.jackrabbit + oak-http + ${project.version} + + + org.apache.jackrabbit + oak-mongomk + ${project.version} + + + com.h2database + h2 + 1.3.158 + + + org.apache.jackrabbit + jackrabbit-jcr-server + ${jackrabbit.version} + + + junit + junit + + + + + org.eclipse.jetty + jetty-servlet + ${jetty.version} + + + ch.qos.logback + logback-classic + 1.0.1 + + + + org.apache.lucene + lucene-core + 4.0.0-ALPHA + + + org.apache.lucene + lucene-analyzers-common + 4.0.0-ALPHA + + + org.apache.tika + tika-core + 1.2 + + + + + junit + junit + test + + + diff --git a/oak-run/src/main/java/org/apache/jackrabbit/oak/run/Main.java b/oak-run/src/main/java/org/apache/jackrabbit/oak/run/Main.java new file mode 100644 index 00000000000..b1df0fd5f98 --- /dev/null +++ b/oak-run/src/main/java/org/apache/jackrabbit/oak/run/Main.java @@ -0,0 +1,278 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.run; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import java.util.concurrent.Executors; + +import javax.jcr.Repository; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.blobs.BlobStore; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +import org.apache.jackrabbit.mongomk.api.NodeStore; +import org.apache.jackrabbit.mongomk.impl.BlobStoreMongo; +import org.apache.jackrabbit.mongomk.impl.MongoConnection; +import org.apache.jackrabbit.mongomk.impl.MongoMicroKernel; +import org.apache.jackrabbit.mongomk.impl.NodeStoreMongo; +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.http.OakServlet; +import org.apache.jackrabbit.oak.jcr.Jcr; +import org.apache.jackrabbit.oak.jcr.RepositoryImpl; +import org.apache.jackrabbit.oak.plugins.commit.ConflictValidatorProvider; +import org.apache.jackrabbit.oak.plugins.index.CompositeIndexHookProvider; +import org.apache.jackrabbit.oak.plugins.index.IndexHookManager; +import org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexHookProvider; +import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexHookProvider; +import org.apache.jackrabbit.oak.plugins.name.NameValidatorProvider; +import org.apache.jackrabbit.oak.plugins.name.NamespaceValidatorProvider; +import org.apache.jackrabbit.oak.plugins.nodetype.DefaultTypeEditor; +import org.apache.jackrabbit.oak.plugins.nodetype.RegistrationValidatorProvider; +import org.apache.jackrabbit.oak.plugins.nodetype.TypeValidatorProvider; +import org.apache.jackrabbit.oak.spi.commit.CommitHook; +import org.apache.jackrabbit.oak.spi.commit.CompositeHook; +import org.apache.jackrabbit.oak.spi.commit.CompositeValidatorProvider; +import org.apache.jackrabbit.oak.spi.commit.ValidatingHook; +import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; +import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +import org.apache.jackrabbit.webdav.jcr.JCRWebdavServerServlet; +import org.apache.jackrabbit.webdav.simple.SimpleWebdavServlet; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; + +public class Main { + + public static final int PORT = 8080; + public static final String URI = "http://localhost:" + PORT + "/"; + + private Main() { + } + + public static void main(String[] args) throws Exception { + printProductInfo(); + + if (args.length > 0 && "mk".equals(args[0])) { + String[] newArgs = new String[args.length - 1]; + System.arraycopy(args, 1, newArgs, 0, newArgs.length); + MicroKernelServer.main(newArgs); + } else { + HttpServer httpServer = new HttpServer(URI, args); + httpServer.start(); + } + } + + private static MongoConnection createDefaultMongoConnection() throws Exception { + // defaults + String host = "localhost"; + int port = 27017; + String database = "MongoMicroKernel"; + String configFile = "config.cfg"; + + // try config file + InputStream is = null; + try { + is = new FileInputStream(configFile); + Properties properties = new Properties(); + properties.load(is); + + host = properties.getProperty("host"); + port = Integer.parseInt(properties.getProperty("port")); + database = properties.getProperty("db"); + } catch (FileNotFoundException e) { + System.out.println("Config file '"+configFile+"' not found, using defaults."); + } catch (IOException e) { + System.out.println("Error while reading '"+configFile+"', using defaults: " + e.getMessage()); + + } finally { + IOUtils.closeQuietly(is); + } + return new MongoConnection(host, port, database); + } + + private static void printProductInfo() { + String version = null; + + try { + InputStream stream = Main.class + .getResourceAsStream("/META-INF/maven/org.apache.jackrabbit/oak-run/pom.properties"); + if (stream != null) { + try { + Properties properties = new Properties(); + properties.load(stream); + version = properties.getProperty("version"); + } finally { + stream.close(); + } + } + } catch (Exception ignore) { + } + + String product; + if (version != null) { + product = "Apache Jackrabbit Oak " + version; + } else { + product = "Apache Jackrabbit Oak"; + } + + System.out.println(product); + } + + public static class HttpServer { + + private final ServletContextHandler context; + + private final Server server; + + private final MicroKernel[] kernels; + + public HttpServer(String uri, String[] args) throws Exception { + int port = java.net.URI.create(uri).getPort(); + if (port == -1) { + // use default + port = PORT; + } + + context = new ServletContextHandler(ServletContextHandler.SECURITY); + context.setContextPath("/"); + + if (args.length == 0) { + System.out.println("Starting an in-memory repository"); + System.out.println(uri + " -> [memory]"); + kernels = new MicroKernel[] { new MicroKernelImpl() }; + addServlets(kernels[0], ""); + } else if (args.length == 1) { + System.out.println("Starting a standalone repository"); + if (args[0].startsWith("mongodb")) { + MongoConnection mongoConnection = createDefaultMongoConnection(); + + mongoConnection.initializeDB(true); + System.out.println(uri + " -> mongodb microkernel " + args[0]); + + NodeStore nodeStore = new NodeStoreMongo(mongoConnection); + BlobStore blobStore = new BlobStoreMongo(mongoConnection); + + kernels = new MicroKernel[] { new MongoMicroKernel(nodeStore, blobStore) }; + + } else { + System.out.println(uri + " -> h2 database " + args[0]); + kernels = new MicroKernel[] { new MicroKernelImpl(args[0]) }; + } + addServlets(kernels[0], ""); + } else { + System.out.println("Starting a clustered repository"); + kernels = new MicroKernel[args.length]; + for (int i = 0; i < args.length; i++) { + // FIXME: Use a clustered MicroKernel implementation + System.out.println(uri + "/node" + i + "/ -> " + args[i]); + kernels[i] = new MicroKernelImpl(args[i]); + addServlets(kernels[i], "/node" + i); + } + } + + server = new Server(port); + server.setHandler(context); + } + + public void start() throws Exception { + server.start(); + } + + public void join() throws Exception { + server.join(); + } + + public void stop() throws Exception { + server.stop(); + } + + private void addServlets(MicroKernel kernel, String path) { + // TODO: review usage of opensecurity provider (using default will cause BasicServerTest to fail. usage of a:a credentials) + SecurityProvider securityProvider = new OpenSecurityProvider(); + ContentRepository repository = new Oak(kernel) + .with(buildDefaultCommitHook()) + .with(securityProvider) + .createContentRepository(); + + ServletHolder oak = + new ServletHolder(new OakServlet(repository)); + context.addServlet(oak, path + "/*"); + + final Repository jcrRepository = new Jcr(kernel).createRepository(); + //new RepositoryImpl( + //repository, Executors.newScheduledThreadPool(1), securityProvider); + + ServletHolder webdav = + new ServletHolder(new SimpleWebdavServlet() { + @Override + public Repository getRepository() { + return jcrRepository; + } + }); + webdav.setInitParameter( + SimpleWebdavServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, + path + "/webdav"); + webdav.setInitParameter( + SimpleWebdavServlet.INIT_PARAM_MISSING_AUTH_MAPPING, + "admin:admin"); + context.addServlet(webdav, path + "/webdav/*"); + + ServletHolder davex = + new ServletHolder(new JCRWebdavServerServlet() { + @Override + protected Repository getRepository() { + return jcrRepository; + } + }); + davex.setInitParameter( + JCRWebdavServerServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, + path + "/davex"); + davex.setInitParameter( + JCRWebdavServerServlet.INIT_PARAM_MISSING_AUTH_MAPPING, + "admin:admin"); + context.addServlet(davex, path + "/davex/*"); + } + + private static CommitHook buildDefaultCommitHook() { + return new CompositeHook( + new DefaultTypeEditor(), + new ValidatingHook(createDefaultValidatorProvider()), + new IndexHookManager( + new CompositeIndexHookProvider( + new PropertyIndexHookProvider(), + new LuceneIndexHookProvider()))); + } + + private static ValidatorProvider createDefaultValidatorProvider() { + return new CompositeValidatorProvider( + new NameValidatorProvider(), + new NamespaceValidatorProvider(), + new TypeValidatorProvider(), + new RegistrationValidatorProvider(), + new ConflictValidatorProvider()); + } + + } + +} diff --git a/oak-run/src/main/java/org/apache/jackrabbit/oak/run/MicroKernelServer.java b/oak-run/src/main/java/org/apache/jackrabbit/oak/run/MicroKernelServer.java new file mode 100644 index 00000000000..f6da1a95f46 --- /dev/null +++ b/oak-run/src/main/java/org/apache/jackrabbit/oak/run/MicroKernelServer.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.run; + +import java.net.InetAddress; + +import org.apache.jackrabbit.mk.api.MicroKernel; +import org.apache.jackrabbit.mk.core.MicroKernelImpl; +import org.apache.jackrabbit.mk.server.Server; + +public class MicroKernelServer { + + public static void main(String[] args) throws Exception { + if (args.length == 0) { + System.out.format("usage: %s /path/to/mk [port] [bindaddr]%n", + MicroKernelServer.class.getName()); + return; + } + + final MicroKernelImpl mk = new MicroKernelImpl(args[0]); + + final Server server = new Server(mk); + if (args.length >= 2) { + server.setPort(Integer.parseInt(args[1])); + } else { + server.setPort(28080); + } + if (args.length >= 3) { + server.setBindAddress(InetAddress.getByName(args[2])); + } + server.start(); + + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + @Override + public void run() { + server.stop(); + mk.dispose(); + } + }, "ShutdownHook")); + } + +} diff --git a/oak-run/src/main/resources/logback.xml b/oak-run/src/main/resources/logback.xml new file mode 100644 index 00000000000..b7554b8e33f --- /dev/null +++ b/oak-run/src/main/resources/logback.xml @@ -0,0 +1,30 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + diff --git a/oak-run/src/test/java/org/apache/jackrabbit/oak/run/BasicServerTest.java b/oak-run/src/test/java/org/apache/jackrabbit/oak/run/BasicServerTest.java new file mode 100644 index 00000000000..2df4885b222 --- /dev/null +++ b/oak-run/src/test/java/org/apache/jackrabbit/oak/run/BasicServerTest.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.run; + +import java.net.HttpURLConnection; +import java.net.URL; + +import org.apache.jackrabbit.util.Base64; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +public class BasicServerTest { + + private static final String SERVER_URL; + + static { + String p = System.getProperty("jetty.http.port"); + if (p != null) { + SERVER_URL = "http://localhost:" + p + "/"; + } else { + SERVER_URL = Main.URI; + } + } + + private Main.HttpServer server; + + @Before + public void startServer() throws Exception { + server = new Main.HttpServer(SERVER_URL, new String[0]); + server.start(); + } + + @After + public void stopServer() throws Exception { + server.stop(); + } + + @Test + public void testServerOk() throws Exception { + + URL server = new URL(SERVER_URL); + HttpURLConnection conn = (HttpURLConnection) server.openConnection(); + conn.setRequestProperty("Authorization", + "Basic " + Base64.encode("a:a")); + assertEquals(200, conn.getResponseCode()); + } +} diff --git a/oak-sling/pom.xml b/oak-sling/pom.xml new file mode 100644 index 00000000000..4d45330f85d --- /dev/null +++ b/oak-sling/pom.xml @@ -0,0 +1,85 @@ + + + + + + 4.0.0 + + + org.apache.jackrabbit + oak-parent + 0.6-SNAPSHOT + ../oak-parent/pom.xml + + + oak-sling + Oak support for Apache Sling + bundle + + + + + org.apache.felix + maven-bundle-plugin + + + + ! + + + oak-jcr + + + org.apache.jackrabbit.oak.sling.Activator + + + + + + + + + + + org.osgi + org.osgi.core + provided + true + + + org.osgi + org.osgi.compendium + provided + true + + + + org.apache.jackrabbit + oak-jcr + ${project.version} + provided + + + + org.apache.sling + org.apache.sling.jcr.api + 2.1.0 + + + + diff --git a/oak-sling/src/main/java/org/apache/jackrabbit/oak/sling/Activator.java b/oak-sling/src/main/java/org/apache/jackrabbit/oak/sling/Activator.java new file mode 100644 index 00000000000..43b9f965dc9 --- /dev/null +++ b/oak-sling/src/main/java/org/apache/jackrabbit/oak/sling/Activator.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.sling; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import javax.jcr.Repository; + +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +import org.apache.sling.jcr.api.SlingRepository; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +public class Activator implements BundleActivator, ServiceTrackerCustomizer { + + private BundleContext context; + + private ScheduledExecutorService executor; + + private SecurityProvider securityProvider; + + private ServiceTracker tracker; + + private final Map jcrRepositories = + new HashMap(); + + private final Map slingRepositories = + new HashMap(); + + //-----------------------------------------------------< BundleActivator >-- + + @Override + public void start(BundleContext bundleContext) throws Exception { + context = bundleContext; + executor = Executors.newScheduledThreadPool(1); + securityProvider = null; // TODO + tracker = new ServiceTracker( + context, ContentRepository.class.getName(), this); + tracker.open(); + } + + @Override + public void stop(BundleContext bundleContext) throws Exception { + tracker.close(); + executor.shutdown(); + } + + //--------------------------------------------< ServiceTrackerCustomizer >-- + + @Override + public Object addingService(ServiceReference reference) { + Object service = context.getService(reference); + if (service instanceof ContentRepository) { + SlingRepository repository = new SlingRepositoryImpl( + (ContentRepository) service, executor, securityProvider); + jcrRepositories.put(reference, context.registerService( + Repository.class.getName(), + repository, new Properties())); + slingRepositories.put(reference, context.registerService( + SlingRepository.class.getName(), + repository, new Properties())); + return service; + } else { + context.ungetService(reference); + return null; + } + } + + @Override + public void modifiedService(ServiceReference reference, Object service) { + } + + @Override + public void removedService(ServiceReference reference, Object service) { + slingRepositories.get(reference).unregister(); + jcrRepositories.get(reference).unregister(); + context.ungetService(reference); + } + +} diff --git a/oak-sling/src/main/java/org/apache/jackrabbit/oak/sling/SlingRepositoryImpl.java b/oak-sling/src/main/java/org/apache/jackrabbit/oak/sling/SlingRepositoryImpl.java new file mode 100644 index 00000000000..5b4fd2833a1 --- /dev/null +++ b/oak-sling/src/main/java/org/apache/jackrabbit/oak/sling/SlingRepositoryImpl.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.sling; + +import java.util.concurrent.ScheduledExecutorService; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.jcr.osgi.OsgiRepository; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +import org.apache.sling.jcr.api.SlingRepository; + +public class SlingRepositoryImpl + extends OsgiRepository implements SlingRepository { + + public SlingRepositoryImpl(ContentRepository repository, + ScheduledExecutorService executor, + SecurityProvider securityProvider) { + super(repository, executor, securityProvider); + } + + @Override + public String getDefaultWorkspace() { + return "default"; + } + + @Override + public Session loginAdministrative(String workspace) + throws RepositoryException { + return login(workspace); + } + +} diff --git a/pom.xml b/pom.xml index 322bee70957..666afa055ed 100644 --- a/pom.xml +++ b/pom.xml @@ -17,20 +17,17 @@ limitations under the License. --> - + 4.0.0 org.apache.jackrabbit oak-parent - 0.1-SNAPSHOT + 0.6-SNAPSHOT oak-parent/pom.xml - oak + jackrabbit-oak Jackrabbit Oak pom @@ -40,10 +37,19 @@ oak-parent + oak-commons + oak-mk-api + oak-mk + oak-mk-remote oak-core + oak-jcr + oak-sling + oak-http + oak-mongomk oak-run oak-it oak-bench + oak-mk-perf @@ -53,18 +59,165 @@ + + + + org.apache.rat + apache-rat-plugin + + + release.properties + .git/** + .idea/** + .gitignore + oak-js/package.json + + + + + + + + - org.apache.rat - apache-rat-plugin - - - release.properties - .git/** - - + org.codehaus.mojo + findbugs-maven-plugin + 2.4.0 - + + + + + mongomk + + oak-mongomk + oak-mongomk-test + oak-mongomk-perf + + + + apache-release + + ${user.name} + ${user.home}/.ssh/id_rsa + + + + + + maven-assembly-plugin + + + + single + + package + + + assembly.xml + + + + + source-release-assembly + + true + + + + + + + maven-antrun-plugin + + + + run + + deploy + + + + + + + + + + + + + + + + + + + + + +From: ${apache.username}@apache.org +To: oak-dev@jackrabbit.apache.org +Subject: [VOTE] Release Apache Jackrabbit Oak ${project.version} + +A candidate for the Jackrabbit Oak ${project.version} release is available at: + + http://people.apache.org/~${apache.username}/oak/${project.version}/ + +The release candidate is a zip archive of the sources in: + + http://svn.apache.org/repos/asf/jackrabbit/oak/tags/${project.artifactId}-${project.version}/ + +The SHA1 checksum of the archive is ${checksum}. + +A staged Maven repository is available for review at: + + https://repository.apache.org/ + +The command for running automated checks against this release candidate is: + + $ sh check-release.sh ${apache.username} ${project.version} ${checksum} + +Please vote on releasing this package as Apache Jackrabbit Oak ${project.version}. +The vote is open for the next 72 hours and passes if a majority of at +least three +1 Jackrabbit PMC votes are cast. + + [ ] +1 Release this package as Apache Jackrabbit Oak ${project.version} + [ ] -1 Do not release this package because...${line.separator} + + + +The release candidate has been prepared in: + + ${basedir}/target/${project.version} + +Please deploy it to people.apache.org like this: + + scp -r ${basedir}/target/${project.version} \ + ${apache.username}@people.apache.org:public_html/oak/ + +A release vote template has been generated for you: + + file://${basedir}/target/vote.txt + + + + + + + + + org.apache.ant + ant-nodeps + 1.8.1 + + + + + + +